1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-31 14:08:06 +08:00

feat: 完成 compose 模版功能

This commit is contained in:
ssongliu 2022-10-17 09:10:06 +08:00 committed by ssongliu
parent e50c4c39c1
commit 56c3c2f4cb
26 changed files with 716 additions and 65 deletions

View File

@ -0,0 +1,100 @@
package v1
import (
"github.com/1Panel-dev/1Panel/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) CreateComposeTemplate(c *gin.Context) {
var req dto.ComposeTemplateCreate
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := composeTemplateService.Create(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) SearchComposeTemplate(c *gin.Context) {
var req dto.PageInfo
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
total, list, err := composeTemplateService.SearchWithPage(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
}
func (b *BaseApi) ListComposeTemplate(c *gin.Context) {
list, err := composeTemplateService.List()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, list)
}
func (b *BaseApi) DeleteComposeTemplate(c *gin.Context) {
var req dto.BatchDeleteReq
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := composeTemplateService.Delete(req.Ids); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) UpdateComposeTemplate(c *gin.Context) {
var req dto.ComposeTemplateUpdate
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
id, err := helper.GetParamID(c)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
upMap := make(map[string]interface{})
upMap["from"] = req.From
upMap["content"] = req.Content
upMap["description"] = req.Description
if err := composeTemplateService.Update(id, upMap); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}

View File

@ -9,17 +9,18 @@ type ApiGroup struct {
var ApiGroupApp = new(ApiGroup)
var (
authService = service.ServiceGroupApp.AuthService
hostService = service.ServiceGroupApp.HostService
backupService = service.ServiceGroupApp.BackupService
groupService = service.ServiceGroupApp.GroupService
containerService = service.ServiceGroupApp.ContainerService
imageRepoService = service.ServiceGroupApp.ImageRepoService
imageService = service.ServiceGroupApp.ImageService
commandService = service.ServiceGroupApp.CommandService
operationService = service.ServiceGroupApp.OperationService
fileService = service.ServiceGroupApp.FileService
cronjobService = service.ServiceGroupApp.CronjobService
settingService = service.ServiceGroupApp.SettingService
appService = service.ServiceGroupApp.AppService
authService = service.ServiceGroupApp.AuthService
hostService = service.ServiceGroupApp.HostService
backupService = service.ServiceGroupApp.BackupService
groupService = service.ServiceGroupApp.GroupService
containerService = service.ServiceGroupApp.ContainerService
composeTemplateService = service.ServiceGroupApp.ComposeTemplateService
imageRepoService = service.ServiceGroupApp.ImageRepoService
imageService = service.ServiceGroupApp.ImageService
commandService = service.ServiceGroupApp.CommandService
operationService = service.ServiceGroupApp.OperationService
fileService = service.ServiceGroupApp.FileService
cronjobService = service.ServiceGroupApp.CronjobService
settingService = service.ServiceGroupApp.SettingService
appService = service.ServiceGroupApp.AppService
)

View File

@ -246,7 +246,7 @@ func (b *BaseApi) LoadFromFile(c *gin.Context) {
return
}
defer file.Close()
buf := make([]byte, 1024*500)
buf := make([]byte, 1024*2)
if _, err := file.Read(buf); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return

View File

@ -0,0 +1,25 @@
package dto
import "time"
type ComposeTemplateCreate struct {
Name string `json:"name" validate:"required"`
From string `json:"from" validate:"required,oneof=edit path"`
Description string `json:"description"`
Content string `json:"content"`
}
type ComposeTemplateUpdate struct {
From string `json:"from" validate:"required,oneof=edit path"`
Description string `json:"description"`
Content string `json:"content"`
}
type ComposeTemplateInfo struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Name string `json:"name"`
From string `json:"from"`
Description string `json:"description"`
Content string `json:"content"`
}

View File

@ -0,0 +1,10 @@
package model
type ComposeTemplate struct {
BaseModel
Name string `gorm:"type:varchar(64);not null;unique" json:"name"`
From string `gorm:"type:varchar(64);not null" json:"from"`
Description string `gorm:"type:varchar(256);" json:"description"`
Content string `gorm:"type:longtext" json:"content"`
}

View File

@ -0,0 +1,69 @@
package repo
import (
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/global"
)
type ComposeTemplateRepo struct{}
type IComposeTemplateRepo interface {
Get(opts ...DBOption) (model.ComposeTemplate, error)
List(opts ...DBOption) ([]model.ComposeTemplate, error)
Page(limit, offset int, opts ...DBOption) (int64, []model.ComposeTemplate, error)
Create(compose *model.ComposeTemplate) error
Update(id uint, vars map[string]interface{}) error
Delete(opts ...DBOption) error
}
func NewIComposeTemplateRepo() IComposeTemplateRepo {
return &ComposeTemplateRepo{}
}
func (u *ComposeTemplateRepo) Get(opts ...DBOption) (model.ComposeTemplate, error) {
var compose model.ComposeTemplate
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&compose).Error
return compose, err
}
func (u *ComposeTemplateRepo) Page(page, size int, opts ...DBOption) (int64, []model.ComposeTemplate, error) {
var users []model.ComposeTemplate
db := global.DB.Model(&model.ComposeTemplate{})
for _, opt := range opts {
db = opt(db)
}
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error
return count, users, err
}
func (u *ComposeTemplateRepo) List(opts ...DBOption) ([]model.ComposeTemplate, error) {
var composes []model.ComposeTemplate
db := global.DB.Model(&model.ComposeTemplate{})
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&composes).Error
return composes, err
}
func (u *ComposeTemplateRepo) Create(compose *model.ComposeTemplate) error {
return global.DB.Create(compose).Error
}
func (u *ComposeTemplateRepo) Update(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.ComposeTemplate{}).Where("id = ?", id).Updates(vars).Error
}
func (u *ComposeTemplateRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.ComposeTemplate{}).Error
}

View File

@ -5,6 +5,7 @@ type RepoGroup struct {
BackupRepo
GroupRepo
ImageRepoRepo
ComposeTemplateRepo
CommandRepo
OperationRepo
CommonRepo

View File

@ -0,0 +1,80 @@
package service
import (
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
)
type ComposeTemplateService struct{}
type IComposeTemplateService interface {
List() ([]dto.ComposeTemplateInfo, error)
SearchWithPage(search dto.PageInfo) (int64, interface{}, error)
Create(composeDto dto.ComposeTemplateCreate) error
Update(id uint, upMap map[string]interface{}) error
Delete(ids []uint) error
}
func NewIComposeTemplateService() IComposeTemplateService {
return &ComposeTemplateService{}
}
func (u *ComposeTemplateService) List() ([]dto.ComposeTemplateInfo, error) {
composes, err := composeRepo.List()
if err != nil {
return nil, constant.ErrRecordNotFound
}
var dtoLists []dto.ComposeTemplateInfo
for _, compose := range composes {
var item dto.ComposeTemplateInfo
if err := copier.Copy(&item, &compose); err != nil {
return nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
}
dtoLists = append(dtoLists, item)
}
return dtoLists, err
}
func (u *ComposeTemplateService) SearchWithPage(search dto.PageInfo) (int64, interface{}, error) {
total, composes, err := composeRepo.Page(search.Page, search.PageSize)
var dtoComposeTemplates []dto.ComposeTemplateInfo
for _, compose := range composes {
var item dto.ComposeTemplateInfo
if err := copier.Copy(&item, &compose); err != nil {
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
}
dtoComposeTemplates = append(dtoComposeTemplates, item)
}
return total, dtoComposeTemplates, err
}
func (u *ComposeTemplateService) Create(composeDto dto.ComposeTemplateCreate) error {
compose, _ := composeRepo.Get(commonRepo.WithByName(composeDto.Name))
if compose.ID != 0 {
return constant.ErrRecordExist
}
if err := copier.Copy(&compose, &composeDto); err != nil {
return errors.WithMessage(constant.ErrStructTransform, err.Error())
}
if err := composeRepo.Create(&compose); err != nil {
return err
}
return nil
}
func (u *ComposeTemplateService) Delete(ids []uint) error {
if len(ids) == 1 {
compose, _ := composeRepo.Get(commonRepo.WithByID(ids[0]))
if compose.ID == 0 {
return constant.ErrRecordNotFound
}
return composeRepo.Delete(commonRepo.WithByID(ids[0]))
}
return composeRepo.Delete(commonRepo.WithIdsIn(ids))
}
func (u *ComposeTemplateService) Update(id uint, upMap map[string]interface{}) error {
return composeRepo.Update(id, upMap)
}

View File

@ -8,6 +8,7 @@ type ServiceGroup struct {
BackupService
GroupService
ImageService
ComposeTemplateService
ImageRepoService
ContainerService
CommandService
@ -28,6 +29,7 @@ var (
operationRepo = repo.RepoGroupApp.OperationRepo
commonRepo = repo.RepoGroupApp.CommonRepo
imageRepoRepo = repo.RepoGroupApp.ImageRepoRepo
composeRepo = repo.RepoGroupApp.ComposeTemplateRepo
cronjobRepo = repo.RepoGroupApp.CronjobRepo
settingRepo = repo.RepoGroupApp.SettingRepo
appRepo = repo.RepoGroupApp.AppRepo
@ -36,6 +38,7 @@ var (
tagRepo = repo.RepoGroupApp.TagRepo
appInstallRepo = repo.RepoGroupApp.AppInstallRepo
appInstallResourceRepo = repo.RepoGroupApp.AppInstallResourceRpo
appContainerRepo = repo.RepoGroupApp.AppContainerRepo
dataBaseRepo = repo.RepoGroupApp.DatabaseRepo
appInstallBackupRepo = repo.RepoGroupApp.AppInstallBackupRepo
)

View File

@ -157,7 +157,7 @@ var AddTableApp = &gormigrate.Migration{
var AddTableImageRepo = &gormigrate.Migration{
ID: "20201009-add-table-imagerepo",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.ImageRepo{}); err != nil {
if err := tx.AutoMigrate(&model.ImageRepo{}, &model.ComposeTemplate{}); err != nil {
return err
}
item := &model.ImageRepo{

View File

@ -33,6 +33,12 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
withRecordRouter.POST("/repo", baseApi.CreateRepo)
withRecordRouter.POST("/repo/del", baseApi.DeleteRepo)
baRouter.POST("/compose/search", baseApi.SearchComposeTemplate)
baRouter.PUT("/compose/:id", baseApi.UpdateComposeTemplate)
baRouter.GET("/compose", baseApi.ListComposeTemplate)
withRecordRouter.POST("/compose", baseApi.CreateComposeTemplate)
withRecordRouter.POST("/compose/del", baseApi.DeleteComposeTemplate)
baRouter.POST("/image/search", baseApi.SearchImage)
baRouter.GET("/image", baseApi.ListImage)
baRouter.POST("/image/pull", baseApi.ImagePull)

View File

@ -161,6 +161,27 @@ export namespace Container {
downloadUrl: string;
}
export interface TemplateCreate {
name: string;
from: string;
description: string;
content: string;
}
export interface TemplateUpdate {
id: number;
from: string;
description: string;
content: string;
}
export interface TemplateInfo {
id: number;
createdAt: Date;
name: string;
from: string;
description: string;
content: string;
}
export interface BatchDelete {
ids: Array<string>;
}

View File

@ -2,33 +2,30 @@ import http from '@/api';
import { ResPage, ReqPage } from '../interface';
import { Container } from '../interface/container';
export const getContainerPage = (params: ReqPage) => {
export const searchContainer = (params: ReqPage) => {
return http.post<ResPage<Container.ContainerInfo>>(`/containers/search`, params);
};
export const createContainer = (params: Container.ContainerCreate) => {
return http.post(`/containers`, params);
};
export const getContainerLog = (params: Container.ContainerLogSearch) => {
export const logContainer = (params: Container.ContainerLogSearch) => {
return http.post<string>(`/containers/log`, params);
};
export const ContainerStats = (id: string) => {
return http.get<Container.ContainerStats>(`/containers/stats/${id}`);
};
export const ContainerOperator = (params: Container.ContainerOperate) => {
return http.post(`/containers/operate`, params);
};
export const inspect = (params: Container.ContainerInspect) => {
return http.post<string>(`/containers/inspect`, params);
};
// image
export const getImagePage = (params: ReqPage) => {
export const searchImage = (params: ReqPage) => {
return http.post<ResPage<Container.ImageInfo>>(`/containers/image/search`, params);
};
export const imageOptions = () => {
export const listImage = () => {
return http.get<Array<Container.Options>>(`/containers/image`);
};
export const imageBuild = (params: Container.ImageBuild) => {
@ -54,7 +51,7 @@ export const imageRemove = (params: Container.BatchDelete) => {
};
// network
export const getNetworkPage = (params: ReqPage) => {
export const searchNetwork = (params: ReqPage) => {
return http.post<ResPage<Container.NetworkInfo>>(`/containers/network/search`, params);
};
export const deleteNetwork = (params: Container.BatchDelete) => {
@ -65,10 +62,10 @@ export const createNetwork = (params: Container.NetworkCreate) => {
};
// volume
export const getVolumePage = (params: ReqPage) => {
export const searchVolume = (params: ReqPage) => {
return http.post<ResPage<Container.VolumeInfo>>(`/containers/volume/search`, params);
};
export const volumeOptions = () => {
export const listVolume = () => {
return http.get<Array<Container.Options>>(`/containers/volume`);
};
export const deleteVolume = (params: Container.BatchDelete) => {
@ -79,18 +76,35 @@ export const createVolume = (params: Container.VolumeCreate) => {
};
// repo
export const getRepoPage = (params: ReqPage) => {
export const searchImageRepo = (params: ReqPage) => {
return http.post<ResPage<Container.RepoInfo>>(`/containers/repo/search`, params);
};
export const getRepoOption = () => {
export const listImageRepo = () => {
return http.get<Container.RepoOptions>(`/containers/repo`);
};
export const repoCreate = (params: Container.RepoCreate) => {
export const createImageRepo = (params: Container.RepoCreate) => {
return http.post(`/containers/repo`, params);
};
export const repoUpdate = (params: Container.RepoUpdate) => {
export const updateImageRepo = (params: Container.RepoUpdate) => {
return http.put(`/containers/repo/${params.id}`, params);
};
export const deleteRepo = (params: { ids: number[] }) => {
export const deleteImageRepo = (params: { ids: number[] }) => {
return http.post(`/containers/repo/del`, params);
};
// composeTemplate
export const searchComposeTemplate = (params: ReqPage) => {
return http.post<ResPage<Container.TemplateInfo>>(`/containers/compose/search`, params);
};
export const listComposeTemplate = (params: ReqPage) => {
return http.post<ResPage<Container.TemplateInfo>>(`/containers/compose/search`, params);
};
export const deleteComposeTemplate = (params: { ids: number[] }) => {
return http.post(`/containers/compose/del`, params);
};
export const createComposeTemplate = (params: Container.TemplateCreate) => {
return http.post(`/containers/compose`, params);
};
export const updateComposeTemplate = (params: Container.TemplateUpdate) => {
return http.put(`/containers/compose/${params.id}`, params);
};

View File

@ -93,6 +93,7 @@ export default {
number: 'Please enter the correct number',
ip: 'Please enter the correct IP address',
port: 'Please enter the correct port',
selectHelper: 'Please select the correct {0} file',
},
res: {
paramError: 'The request failed, please try again later!',
@ -161,7 +162,6 @@ export default {
reName: 'ReName',
remove: 'Remove',
container: 'Container',
schedule: 'Schedule',
upTime: 'UpTime',
all: 'All',
lastDay: 'Last Day',
@ -242,6 +242,11 @@ export default {
imageRepo: 'Image repo',
repoHelper: 'Does it include a mirror repository/organization/project?',
auth: 'Auth',
compose: 'Compose',
composeTemplate: 'Compose template',
description: 'Description',
content: 'Content',
},
cronjob: {
cronTask: 'Task',

View File

@ -2,6 +2,7 @@ export default {
commons: {
true: '是',
false: '否',
example: '例如',
button: {
create: '新建',
add: '添加',
@ -90,6 +91,7 @@ export default {
number: '请输入正确的数字',
ip: '请输入正确的 IP 地址',
port: '请输入正确的端口',
selectHelper: '请选择正确的 {0} 文件',
},
res: {
paramError: '请求失败,请稍后重试!',
@ -158,7 +160,6 @@ export default {
reName: '重命名',
remove: '移除',
container: '容器',
schedule: '编排',
upTime: '运行时长',
all: '全部',
lastDay: '最近一天',
@ -242,6 +243,11 @@ export default {
imageRepo: '镜像仓库',
repoHelper: '是否包含镜像仓库/组织/项目?',
auth: '认证',
compose: '编排',
composeTemplate: '编排模版',
description: '描述',
content: '内容',
},
cronjob: {
cronTask: '计划任务',

View File

@ -0,0 +1,169 @@
<template>
<div>
<el-card style="margin-top: 20px">
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
<template #toolbar>
<el-button type="primary" @click="onOpenDialog('create')">
{{ $t('commons.button.create') }}
</el-button>
<el-button type="danger" plain :disabled="selects.length === 0" @click="onBatchDelete(null)">
{{ $t('commons.button.delete') }}
</el-button>
</template>
<el-table-column type="selection" fix></el-table-column>
<el-table-column
:label="$t('commons.table.name')"
show-overflow-tooltip
min-width="100"
prop="name"
fix
>
<template #default="{ row }">
<el-link @click="onOpenDetail(row)" type="primary">{{ row.name }}</el-link>
</template>
</el-table-column>
<el-table-column :label="$t('container.description')" prop="description" min-width="200" fix />
<el-table-column :label="$t('commons.table.createdAt')" min-width="80" fix>
<template #default="{ row }">
{{ dateFromat(0, 0, row.createdAt) }}
</template>
</el-table-column>
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" />
</ComplexTable>
</el-card>
<el-dialog v-model="detailVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="70%">
<template #header>
<div class="card-header">
<span>{{ $t('commons.button.view') }}</span>
</div>
</template>
<codemirror
:autofocus="true"
placeholder="None data"
:indent-with-tab="true"
:tabSize="4"
style="max-height: 500px"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
:styleActiveLine="true"
:extensions="extensions"
v-model="detailInfo"
:readOnly="true"
/>
<template #footer>
<span class="dialog-footer">
<el-button @click="detailVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
</span>
</template>
</el-dialog>
<OperatorDialog @search="search" ref="dialogRef" />
</div>
</template>
<script lang="ts" setup>
import ComplexTable from '@/components/complex-table/index.vue';
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
import { reactive, onMounted, ref } from 'vue';
import { dateFromat } from '@/utils/util';
import { Container } from '@/api/interface/container';
import OperatorDialog from '@/views/container/compose/template/operator/index.vue';
import { deleteComposeTemplate, searchComposeTemplate } from '@/api/modules/container';
import { useDeleteData } from '@/hooks/use-delete-data';
import i18n from '@/lang';
import { LoadFile } from '@/api/modules/files';
const data = ref();
const selects = ref<any>([]);
const detailVisiable = ref(false);
const detailInfo = ref();
const extensions = [javascript(), oneDark];
const paginationConfig = reactive({
page: 1,
pageSize: 10,
total: 0,
});
const search = async () => {
let params = {
page: paginationConfig.page,
pageSize: paginationConfig.pageSize,
};
await searchComposeTemplate(params).then((res) => {
if (res.data) {
data.value = res.data.items;
}
});
};
const onOpenDetail = async (row: Container.TemplateInfo) => {
if (row.from === 'edit') {
detailInfo.value = row.content;
detailVisiable.value = true;
} else {
const res = await LoadFile({ path: row.content });
detailInfo.value = res.data;
detailVisiable.value = true;
}
};
const dialogRef = ref();
const onOpenDialog = async (
title: string,
rowData: Partial<Container.TemplateInfo> = {
name: '',
from: 'edit',
description: '',
content: '',
},
) => {
let params = {
title,
rowData: { ...rowData },
};
dialogRef.value!.acceptParams(params);
};
const onBatchDelete = async (row: Container.RepoInfo | null) => {
let ids: Array<number> = [];
if (row) {
ids.push(row.id);
} else {
selects.value.forEach((item: Container.RepoInfo) => {
ids.push(item.id);
});
}
await useDeleteData(deleteComposeTemplate, { ids: ids }, 'commons.msg.delete', true);
search();
};
const buttons = [
{
label: i18n.global.t('commons.button.edit'),
disabled: (row: Container.RepoInfo) => {
return row.downloadUrl === 'docker.io';
},
click: (row: Container.RepoInfo) => {
onOpenDialog('edit', row);
},
},
{
label: i18n.global.t('commons.button.delete'),
disabled: (row: Container.RepoInfo) => {
return row.downloadUrl === 'docker.io';
},
click: (row: Container.RepoInfo) => {
onBatchDelete(row);
},
},
];
onMounted(() => {
search();
});
</script>

View File

@ -0,0 +1,133 @@
<template>
<el-dialog v-model="templateVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="50%">
<template #header>
<div class="card-header">
<span>{{ title }}{{ $t('container.composeTemplate') }}</span>
</div>
</template>
<el-form ref="formRef" :model="dialogData.rowData" :rules="rules" label-width="80px">
<el-form-item :label="$t('container.name')" prop="name">
<el-input :disabled="dialogData.title === 'edit'" v-model="dialogData.rowData!.name"></el-input>
</el-form-item>
<el-form-item :label="$t('container.description')">
<el-input v-model="dialogData.rowData!.description"></el-input>
</el-form-item>
<el-form-item :label="$t('container.from')">
<el-radio-group v-model="dialogData.rowData!.from">
<el-radio label="edit">{{ $t('container.edit') }}</el-radio>
<el-radio label="path">{{ $t('container.pathSelect') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="dialogData.rowData!.from === 'path'" :rules="Rules.requiredSelect" prop="content">
<el-input
clearable
:placeholder="$t('commons.example') + '/tmp/docker-compose.yml'"
v-model="dialogData.rowData!.content"
>
<template #append>
<FileList @choose="loadDir" :dir="false"></FileList>
</template>
</el-input>
</el-form-item>
<el-form-item v-if="dialogData.rowData!.from === 'edit'">
<codemirror
:autofocus="true"
placeholder="None data"
:indent-with-tab="true"
:tabSize="4"
style="max-height: 500px; width: 100%"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
:styleActiveLine="true"
:extensions="extensions"
v-model="dialogData.rowData!.content"
:readOnly="true"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="templateVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import FileList from '@/components/file-list/index.vue';
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { ElForm, ElMessage } from 'element-plus';
import { Container } from '@/api/interface/container';
import { createComposeTemplate, updateComposeTemplate } from '@/api/modules/container';
interface DialogProps {
title: string;
rowData?: Container.TemplateInfo;
getTableList?: () => Promise<any>;
}
const extensions = [javascript(), oneDark];
const title = ref<string>('');
const templateVisiable = ref(false);
const dialogData = ref<DialogProps>({
title: '',
});
const acceptParams = (params: DialogProps): void => {
dialogData.value = params;
title.value = i18n.global.t('commons.button.' + dialogData.value.title);
templateVisiable.value = true;
};
const emit = defineEmits<{ (e: 'search'): void }>();
const varifyPath = (rule: any, value: any, callback: any) => {
if (value.indexOf('docker-compose.yml') === -1) {
callback(new Error(i18n.global.t('commons.rule.selectHelper', ['.docker-compose.yml'])));
}
callback();
};
const rules = reactive({
name: [Rules.requiredInput, Rules.name],
content: [Rules.requiredInput, { validator: varifyPath, trigger: 'change', required: true }],
});
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
function restForm() {
if (formRef.value) {
formRef.value.resetFields();
}
}
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
if (dialogData.value.title === 'create') {
await createComposeTemplate(dialogData.value.rowData!);
}
if (dialogData.value.title === 'edit') {
await updateComposeTemplate(dialogData.value.rowData!);
}
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
restForm();
emit('search');
templateVisiable.value = false;
});
};
const loadDir = async (path: string) => {
dialogData.value.rowData!.content = path;
};
defineExpose({
acceptParams,
});
</script>

View File

@ -189,7 +189,7 @@ import { reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { ElForm, ElMessage } from 'element-plus';
import { imageOptions, volumeOptions, createContainer } from '@/api/modules/container';
import { listImage, listVolume, createContainer } from '@/api/modules/container';
import { Container } from '@/api/interface/container';
const createVisiable = ref(false);
@ -259,11 +259,11 @@ const handleVolumesDelete = (index: number) => {
};
const loadImageOptions = async () => {
const res = await imageOptions();
const res = await listImage();
images.value = res.data;
};
const loadVolumeOptions = async () => {
const res = await volumeOptions();
const res = await listVolume();
volumes.value = res.data;
};
const onSubmit = async (formEl: FormInstance | undefined) => {

View File

@ -171,7 +171,7 @@ import { oneDark } from '@codemirror/theme-one-dark';
import { reactive, onMounted, ref } from 'vue';
import { dateFromat, dateFromatForName } from '@/utils/util';
import { Rules } from '@/global/form-rules';
import { ContainerOperator, inspect, getContainerLog, getContainerPage } from '@/api/modules/container';
import { ContainerOperator, inspect, logContainer, searchContainer } from '@/api/modules/container';
import { Container } from '@/api/interface/container';
import { ElForm, ElMessage, ElMessageBox, FormInstance } from 'element-plus';
import i18n from '@/lang';
@ -234,7 +234,7 @@ const timeOptions = ref([
const search = async () => {
containerSearch.page = paginationConfig.page;
containerSearch.pageSize = paginationConfig.pageSize;
await getContainerPage(containerSearch).then((res) => {
await searchContainer(containerSearch).then((res) => {
if (res.data) {
data.value = res.data.items;
}
@ -272,7 +272,7 @@ const onCloseLog = async () => {
clearInterval(Number(timer));
};
const searchLogs = async () => {
const res = await getContainerLog(logSearch);
const res = await logContainer(logSearch);
logInfo.value = res.data;
};
const onDownload = async () => {

View File

@ -93,6 +93,16 @@ const form = reactive({
tagStr: '',
tags: [] as Array<string>,
});
const varifyPath = (rule: any, value: any, callback: any) => {
if (value.indexOf('docker-compose.yml') === -1) {
callback(new Error(i18n.global.t('commons.rule.selectHelper', ['Dockerfile'])));
}
callback();
};
const rules = reactive({
name: [Rules.requiredInput, Rules.name],
content: [Rules.requiredInput, { validator: varifyPath, trigger: 'change', required: true }],
});
const acceptParams = async () => {
buildVisiable.value = true;
form.from = 'path';

View File

@ -78,7 +78,7 @@ import Push from '@/views/container/image/push/index.vue';
import Save from '@/views/container/image/save/index.vue';
import Load from '@/views/container/image/load/index.vue';
import Build from '@/views/container/image/build/index.vue';
import { getImagePage, getRepoOption, imageRemove } from '@/api/modules/container';
import { searchImage, listImageRepo, imageRemove } from '@/api/modules/container';
import i18n from '@/lang';
import { ElForm } from 'element-plus';
import { useDeleteData } from '@/hooks/use-delete-data';
@ -112,7 +112,7 @@ const search = async () => {
page: paginationConfig.page,
pageSize: paginationConfig.pageSize,
};
await getImagePage(repoSearch).then((res) => {
await searchImage(repoSearch).then((res) => {
if (res.data) {
data.value = res.data.items;
}
@ -120,7 +120,7 @@ const search = async () => {
});
};
const loadRepos = async () => {
const res = await getRepoOption();
const res = await listImageRepo();
repos.value = res.data;
};

View File

@ -17,8 +17,11 @@
<el-radio-button class="topButton" size="large" label="repo">
{{ $t('container.repo') }}
</el-radio-button>
<el-radio-button class="topButton" size="large" label="schedule">
{{ $t('container.schedule') }}
<el-radio-button class="topButton" size="large" label="compose">
{{ $t('container.compose') }}
</el-radio-button>
<el-radio-button class="topButton" size="large" label="composeTemplate">
{{ $t('container.composeTemplate') }}
</el-radio-button>
</el-radio-group>
</el-card>
@ -27,7 +30,7 @@
<Image v-if="activeNames === 'image'" />
<Network v-if="activeNames === 'network'" />
<Volume v-if="activeNames === 'volume'" />
<About v-if="activeNames === 'schedule'" />
<ComposeTemplate v-if="activeNames === 'composeTemplate'" />
</div>
</template>
@ -38,7 +41,7 @@ import Repo from '@/views/container/repo/index.vue';
import Image from '@/views/container/image/index.vue';
import Network from '@/views/container/network/index.vue';
import Volume from '@/views/container/volume/index.vue';
import About from '@/views/setting/tabs/about.vue';
import ComposeTemplate from '@/views/container/compose/template/index.vue';
const activeNames = ref('container');
</script>

View File

@ -83,7 +83,7 @@ import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
import { reactive, onMounted, ref } from 'vue';
import { dateFromat } from '@/utils/util';
import { deleteNetwork, getNetworkPage, inspect } from '@/api/modules/container';
import { deleteNetwork, searchNetwork, inspect } from '@/api/modules/container';
import { Container } from '@/api/interface/container';
import i18n from '@/lang';
import { useDeleteData } from '@/hooks/use-delete-data';
@ -114,7 +114,7 @@ const search = async () => {
page: paginationConfig.page,
pageSize: paginationConfig.pageSize,
};
await getNetworkPage(params).then((res) => {
await searchNetwork(params).then((res) => {
if (res.data) {
data.value = res.data.items;
}

View File

@ -38,7 +38,7 @@ import OperatorDialog from '@/views/container/repo/operator/index.vue';
import { reactive, onMounted, ref } from 'vue';
import { dateFromat } from '@/utils/util';
import { Container } from '@/api/interface/container';
import { deleteRepo, getRepoPage } from '@/api/modules/container';
import { deleteImageRepo, searchImageRepo } from '@/api/modules/container';
import { useDeleteData } from '@/hooks/use-delete-data';
import i18n from '@/lang';
@ -49,25 +49,20 @@ const paginationConfig = reactive({
pageSize: 10,
total: 0,
});
const repoSearch = reactive({
page: 1,
pageSize: 5,
});
const search = async () => {
repoSearch.page = paginationConfig.page;
repoSearch.pageSize = paginationConfig.pageSize;
await getRepoPage(repoSearch).then((res) => {
let params = {
page: paginationConfig.page,
pageSize: paginationConfig.pageSize,
};
await searchImageRepo(params).then((res) => {
if (res.data) {
data.value = res.data.items;
}
});
};
interface DialogExpose {
acceptParams: (params: any) => void;
}
const dialogRef = ref<DialogExpose>();
const dialogRef = ref();
const onOpenDialog = async (
title: string,
rowData: Partial<Container.RepoInfo> = {
@ -91,7 +86,7 @@ const onBatchDelete = async (row: Container.RepoInfo | null) => {
ids.push(item.id);
});
}
await useDeleteData(deleteRepo, { ids: ids }, 'commons.msg.delete', true);
await useDeleteData(deleteImageRepo, { ids: ids }, 'commons.msg.delete', true);
search();
};

View File

@ -48,7 +48,7 @@ import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { ElForm, ElMessage } from 'element-plus';
import { Container } from '@/api/interface/container';
import { repoCreate, repoUpdate } from '@/api/modules/container';
import { createImageRepo, updateImageRepo } from '@/api/modules/container';
interface DialogProps {
title: string;
@ -89,10 +89,10 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
formEl.validate(async (valid) => {
if (!valid) return;
if (dialogData.value.title === 'create') {
await repoCreate(dialogData.value.rowData!);
await createImageRepo(dialogData.value.rowData!);
}
if (dialogData.value.title === 'edit') {
await repoUpdate(dialogData.value.rowData!);
await updateImageRepo(dialogData.value.rowData!);
}
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
restForm();

View File

@ -72,7 +72,7 @@ import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
import { reactive, onMounted, ref } from 'vue';
import { dateFromat } from '@/utils/util';
import { deleteVolume, getVolumePage, inspect } from '@/api/modules/container';
import { deleteVolume, searchVolume, inspect } from '@/api/modules/container';
import { Container } from '@/api/interface/container';
import i18n from '@/lang';
import { useDeleteData } from '@/hooks/use-delete-data';
@ -103,7 +103,7 @@ const search = async () => {
page: paginationConfig.page,
pageSize: paginationConfig.pageSize,
};
await getVolumePage(params).then((res) => {
await searchVolume(params).then((res) => {
if (res.data) {
data.value = res.data.items;
}