1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-03-01 03:24:14 +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

@ -14,6 +14,7 @@ var (
backupService = service.ServiceGroupApp.BackupService backupService = service.ServiceGroupApp.BackupService
groupService = service.ServiceGroupApp.GroupService groupService = service.ServiceGroupApp.GroupService
containerService = service.ServiceGroupApp.ContainerService containerService = service.ServiceGroupApp.ContainerService
composeTemplateService = service.ServiceGroupApp.ComposeTemplateService
imageRepoService = service.ServiceGroupApp.ImageRepoService imageRepoService = service.ServiceGroupApp.ImageRepoService
imageService = service.ServiceGroupApp.ImageService imageService = service.ServiceGroupApp.ImageService
commandService = service.ServiceGroupApp.CommandService commandService = service.ServiceGroupApp.CommandService

View File

@ -246,7 +246,7 @@ func (b *BaseApi) LoadFromFile(c *gin.Context) {
return return
} }
defer file.Close() defer file.Close()
buf := make([]byte, 1024*500) buf := make([]byte, 1024*2)
if _, err := file.Read(buf); err != nil { if _, err := file.Read(buf); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return 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 BackupRepo
GroupRepo GroupRepo
ImageRepoRepo ImageRepoRepo
ComposeTemplateRepo
CommandRepo CommandRepo
OperationRepo OperationRepo
CommonRepo 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 BackupService
GroupService GroupService
ImageService ImageService
ComposeTemplateService
ImageRepoService ImageRepoService
ContainerService ContainerService
CommandService CommandService
@ -28,6 +29,7 @@ var (
operationRepo = repo.RepoGroupApp.OperationRepo operationRepo = repo.RepoGroupApp.OperationRepo
commonRepo = repo.RepoGroupApp.CommonRepo commonRepo = repo.RepoGroupApp.CommonRepo
imageRepoRepo = repo.RepoGroupApp.ImageRepoRepo imageRepoRepo = repo.RepoGroupApp.ImageRepoRepo
composeRepo = repo.RepoGroupApp.ComposeTemplateRepo
cronjobRepo = repo.RepoGroupApp.CronjobRepo cronjobRepo = repo.RepoGroupApp.CronjobRepo
settingRepo = repo.RepoGroupApp.SettingRepo settingRepo = repo.RepoGroupApp.SettingRepo
appRepo = repo.RepoGroupApp.AppRepo appRepo = repo.RepoGroupApp.AppRepo
@ -36,6 +38,7 @@ var (
tagRepo = repo.RepoGroupApp.TagRepo tagRepo = repo.RepoGroupApp.TagRepo
appInstallRepo = repo.RepoGroupApp.AppInstallRepo appInstallRepo = repo.RepoGroupApp.AppInstallRepo
appInstallResourceRepo = repo.RepoGroupApp.AppInstallResourceRpo appInstallResourceRepo = repo.RepoGroupApp.AppInstallResourceRpo
appContainerRepo = repo.RepoGroupApp.AppContainerRepo
dataBaseRepo = repo.RepoGroupApp.DatabaseRepo dataBaseRepo = repo.RepoGroupApp.DatabaseRepo
appInstallBackupRepo = repo.RepoGroupApp.AppInstallBackupRepo appInstallBackupRepo = repo.RepoGroupApp.AppInstallBackupRepo
) )

View File

@ -157,7 +157,7 @@ var AddTableApp = &gormigrate.Migration{
var AddTableImageRepo = &gormigrate.Migration{ var AddTableImageRepo = &gormigrate.Migration{
ID: "20201009-add-table-imagerepo", ID: "20201009-add-table-imagerepo",
Migrate: func(tx *gorm.DB) error { 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 return err
} }
item := &model.ImageRepo{ item := &model.ImageRepo{

View File

@ -33,6 +33,12 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
withRecordRouter.POST("/repo", baseApi.CreateRepo) withRecordRouter.POST("/repo", baseApi.CreateRepo)
withRecordRouter.POST("/repo/del", baseApi.DeleteRepo) 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.POST("/image/search", baseApi.SearchImage)
baRouter.GET("/image", baseApi.ListImage) baRouter.GET("/image", baseApi.ListImage)
baRouter.POST("/image/pull", baseApi.ImagePull) baRouter.POST("/image/pull", baseApi.ImagePull)

View File

@ -161,6 +161,27 @@ export namespace Container {
downloadUrl: string; 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 { export interface BatchDelete {
ids: Array<string>; ids: Array<string>;
} }

View File

@ -2,33 +2,30 @@ import http from '@/api';
import { ResPage, ReqPage } from '../interface'; import { ResPage, ReqPage } from '../interface';
import { Container } from '../interface/container'; import { Container } from '../interface/container';
export const getContainerPage = (params: ReqPage) => { export const searchContainer = (params: ReqPage) => {
return http.post<ResPage<Container.ContainerInfo>>(`/containers/search`, params); return http.post<ResPage<Container.ContainerInfo>>(`/containers/search`, params);
}; };
export const createContainer = (params: Container.ContainerCreate) => { export const createContainer = (params: Container.ContainerCreate) => {
return http.post(`/containers`, params); return http.post(`/containers`, params);
}; };
export const logContainer = (params: Container.ContainerLogSearch) => {
export const getContainerLog = (params: Container.ContainerLogSearch) => {
return http.post<string>(`/containers/log`, params); return http.post<string>(`/containers/log`, params);
}; };
export const ContainerStats = (id: string) => { export const ContainerStats = (id: string) => {
return http.get<Container.ContainerStats>(`/containers/stats/${id}`); return http.get<Container.ContainerStats>(`/containers/stats/${id}`);
}; };
export const ContainerOperator = (params: Container.ContainerOperate) => { export const ContainerOperator = (params: Container.ContainerOperate) => {
return http.post(`/containers/operate`, params); return http.post(`/containers/operate`, params);
}; };
export const inspect = (params: Container.ContainerInspect) => { export const inspect = (params: Container.ContainerInspect) => {
return http.post<string>(`/containers/inspect`, params); return http.post<string>(`/containers/inspect`, params);
}; };
// image // image
export const getImagePage = (params: ReqPage) => { export const searchImage = (params: ReqPage) => {
return http.post<ResPage<Container.ImageInfo>>(`/containers/image/search`, params); return http.post<ResPage<Container.ImageInfo>>(`/containers/image/search`, params);
}; };
export const imageOptions = () => { export const listImage = () => {
return http.get<Array<Container.Options>>(`/containers/image`); return http.get<Array<Container.Options>>(`/containers/image`);
}; };
export const imageBuild = (params: Container.ImageBuild) => { export const imageBuild = (params: Container.ImageBuild) => {
@ -54,7 +51,7 @@ export const imageRemove = (params: Container.BatchDelete) => {
}; };
// network // network
export const getNetworkPage = (params: ReqPage) => { export const searchNetwork = (params: ReqPage) => {
return http.post<ResPage<Container.NetworkInfo>>(`/containers/network/search`, params); return http.post<ResPage<Container.NetworkInfo>>(`/containers/network/search`, params);
}; };
export const deleteNetwork = (params: Container.BatchDelete) => { export const deleteNetwork = (params: Container.BatchDelete) => {
@ -65,10 +62,10 @@ export const createNetwork = (params: Container.NetworkCreate) => {
}; };
// volume // volume
export const getVolumePage = (params: ReqPage) => { export const searchVolume = (params: ReqPage) => {
return http.post<ResPage<Container.VolumeInfo>>(`/containers/volume/search`, params); return http.post<ResPage<Container.VolumeInfo>>(`/containers/volume/search`, params);
}; };
export const volumeOptions = () => { export const listVolume = () => {
return http.get<Array<Container.Options>>(`/containers/volume`); return http.get<Array<Container.Options>>(`/containers/volume`);
}; };
export const deleteVolume = (params: Container.BatchDelete) => { export const deleteVolume = (params: Container.BatchDelete) => {
@ -79,18 +76,35 @@ export const createVolume = (params: Container.VolumeCreate) => {
}; };
// repo // repo
export const getRepoPage = (params: ReqPage) => { export const searchImageRepo = (params: ReqPage) => {
return http.post<ResPage<Container.RepoInfo>>(`/containers/repo/search`, params); return http.post<ResPage<Container.RepoInfo>>(`/containers/repo/search`, params);
}; };
export const getRepoOption = () => { export const listImageRepo = () => {
return http.get<Container.RepoOptions>(`/containers/repo`); 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); 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); 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); 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', number: 'Please enter the correct number',
ip: 'Please enter the correct IP address', ip: 'Please enter the correct IP address',
port: 'Please enter the correct port', port: 'Please enter the correct port',
selectHelper: 'Please select the correct {0} file',
}, },
res: { res: {
paramError: 'The request failed, please try again later!', paramError: 'The request failed, please try again later!',
@ -161,7 +162,6 @@ export default {
reName: 'ReName', reName: 'ReName',
remove: 'Remove', remove: 'Remove',
container: 'Container', container: 'Container',
schedule: 'Schedule',
upTime: 'UpTime', upTime: 'UpTime',
all: 'All', all: 'All',
lastDay: 'Last Day', lastDay: 'Last Day',
@ -242,6 +242,11 @@ export default {
imageRepo: 'Image repo', imageRepo: 'Image repo',
repoHelper: 'Does it include a mirror repository/organization/project?', repoHelper: 'Does it include a mirror repository/organization/project?',
auth: 'Auth', auth: 'Auth',
compose: 'Compose',
composeTemplate: 'Compose template',
description: 'Description',
content: 'Content',
}, },
cronjob: { cronjob: {
cronTask: 'Task', cronTask: 'Task',

View File

@ -2,6 +2,7 @@ export default {
commons: { commons: {
true: '是', true: '是',
false: '否', false: '否',
example: '例如',
button: { button: {
create: '新建', create: '新建',
add: '添加', add: '添加',
@ -90,6 +91,7 @@ export default {
number: '请输入正确的数字', number: '请输入正确的数字',
ip: '请输入正确的 IP 地址', ip: '请输入正确的 IP 地址',
port: '请输入正确的端口', port: '请输入正确的端口',
selectHelper: '请选择正确的 {0} 文件',
}, },
res: { res: {
paramError: '请求失败,请稍后重试!', paramError: '请求失败,请稍后重试!',
@ -158,7 +160,6 @@ export default {
reName: '重命名', reName: '重命名',
remove: '移除', remove: '移除',
container: '容器', container: '容器',
schedule: '编排',
upTime: '运行时长', upTime: '运行时长',
all: '全部', all: '全部',
lastDay: '最近一天', lastDay: '最近一天',
@ -242,6 +243,11 @@ export default {
imageRepo: '镜像仓库', imageRepo: '镜像仓库',
repoHelper: '是否包含镜像仓库/组织/项目?', repoHelper: '是否包含镜像仓库/组织/项目?',
auth: '认证', auth: '认证',
compose: '编排',
composeTemplate: '编排模版',
description: '描述',
content: '内容',
}, },
cronjob: { cronjob: {
cronTask: '计划任务', 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 { Rules } from '@/global/form-rules';
import i18n from '@/lang'; import i18n from '@/lang';
import { ElForm, ElMessage } from 'element-plus'; 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'; import { Container } from '@/api/interface/container';
const createVisiable = ref(false); const createVisiable = ref(false);
@ -259,11 +259,11 @@ const handleVolumesDelete = (index: number) => {
}; };
const loadImageOptions = async () => { const loadImageOptions = async () => {
const res = await imageOptions(); const res = await listImage();
images.value = res.data; images.value = res.data;
}; };
const loadVolumeOptions = async () => { const loadVolumeOptions = async () => {
const res = await volumeOptions(); const res = await listVolume();
volumes.value = res.data; volumes.value = res.data;
}; };
const onSubmit = async (formEl: FormInstance | undefined) => { 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 { reactive, onMounted, ref } from 'vue';
import { dateFromat, dateFromatForName } from '@/utils/util'; import { dateFromat, dateFromatForName } from '@/utils/util';
import { Rules } from '@/global/form-rules'; 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 { Container } from '@/api/interface/container';
import { ElForm, ElMessage, ElMessageBox, FormInstance } from 'element-plus'; import { ElForm, ElMessage, ElMessageBox, FormInstance } from 'element-plus';
import i18n from '@/lang'; import i18n from '@/lang';
@ -234,7 +234,7 @@ const timeOptions = ref([
const search = async () => { const search = async () => {
containerSearch.page = paginationConfig.page; containerSearch.page = paginationConfig.page;
containerSearch.pageSize = paginationConfig.pageSize; containerSearch.pageSize = paginationConfig.pageSize;
await getContainerPage(containerSearch).then((res) => { await searchContainer(containerSearch).then((res) => {
if (res.data) { if (res.data) {
data.value = res.data.items; data.value = res.data.items;
} }
@ -272,7 +272,7 @@ const onCloseLog = async () => {
clearInterval(Number(timer)); clearInterval(Number(timer));
}; };
const searchLogs = async () => { const searchLogs = async () => {
const res = await getContainerLog(logSearch); const res = await logContainer(logSearch);
logInfo.value = res.data; logInfo.value = res.data;
}; };
const onDownload = async () => { const onDownload = async () => {

View File

@ -93,6 +93,16 @@ const form = reactive({
tagStr: '', tagStr: '',
tags: [] as Array<string>, 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 () => { const acceptParams = async () => {
buildVisiable.value = true; buildVisiable.value = true;
form.from = 'path'; 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 Save from '@/views/container/image/save/index.vue';
import Load from '@/views/container/image/load/index.vue'; import Load from '@/views/container/image/load/index.vue';
import Build from '@/views/container/image/build/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 i18n from '@/lang';
import { ElForm } from 'element-plus'; import { ElForm } from 'element-plus';
import { useDeleteData } from '@/hooks/use-delete-data'; import { useDeleteData } from '@/hooks/use-delete-data';
@ -112,7 +112,7 @@ const search = async () => {
page: paginationConfig.page, page: paginationConfig.page,
pageSize: paginationConfig.pageSize, pageSize: paginationConfig.pageSize,
}; };
await getImagePage(repoSearch).then((res) => { await searchImage(repoSearch).then((res) => {
if (res.data) { if (res.data) {
data.value = res.data.items; data.value = res.data.items;
} }
@ -120,7 +120,7 @@ const search = async () => {
}); });
}; };
const loadRepos = async () => { const loadRepos = async () => {
const res = await getRepoOption(); const res = await listImageRepo();
repos.value = res.data; repos.value = res.data;
}; };

View File

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

View File

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

View File

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

View File

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