feat: 完成镜像仓库管理

This commit is contained in:
ssongliu 2022-10-09 16:17:15 +08:00 committed by ssongliu
parent a79ba71ef4
commit dcbf92ac12
22 changed files with 848 additions and 50 deletions

View File

@ -14,6 +14,7 @@ var (
backupService = service.ServiceGroupApp.BackupService
groupService = service.ServiceGroupApp.GroupService
containerService = service.ServiceGroupApp.ContainerService
imageRepoService = service.ServiceGroupApp.ImageRepoService
commandService = service.ServiceGroupApp.CommandService
operationService = service.ServiceGroupApp.OperationService
fileService = service.ServiceGroupApp.FileService

View File

@ -0,0 +1,92 @@
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) GetRepoList(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 := imageRepoService.Page(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) CreateRepo(c *gin.Context) {
var req dto.ImageRepoCreate
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 := imageRepoService.Create(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) DeleteRepo(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 := imageRepoService.BatchDelete(req.Ids); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) UpdateRepo(c *gin.Context) {
var req dto.ImageRepoUpdate
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["download_url"] = req.DownloadUrl
upMap["repo_name"] = req.RepoName
upMap["username"] = req.Username
upMap["password"] = req.Password
upMap["auth"] = req.Auth
if err := imageRepoService.Update(id, upMap); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}

View File

@ -0,0 +1,31 @@
package dto
import "time"
type ImageRepoCreate struct {
Name string `json:"name" validate:"required"`
DownloadUrl string `json:"downloadUrl"`
RepoName string `json:"repoName"`
Username string `json:"username"`
Password string `json:"password"`
Auth bool `json:"auth"`
}
type ImageRepoUpdate struct {
ID uint `json:"id"`
DownloadUrl string `json:"downloadUrl"`
RepoName string `json:"repoName"`
Username string `json:"username"`
Password string `json:"password"`
Auth bool `json:"auth"`
}
type ImageRepoInfo struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Name string `json:"name"`
DownloadUrl string `json:"downloadUrl"`
RepoName string `json:"repoName"`
Username string `json:"username"`
Auth bool `json:"auth"`
}

View File

@ -0,0 +1,12 @@
package model
type ImageRepo struct {
BaseModel
Name string `gorm:"type:varchar(64);not null" json:"name"`
DownloadUrl string `gorm:"type:varchar(256)" json:"downloadUrl"`
RepoName string `gorm:"type:varchar(256)" json:"repoName"`
Username string `gorm:"type:varchar(256)" json:"username"`
Password string `gorm:"type:varchar(256)" json:"password"`
Auth bool `gorm:"type:varchar(256)" json:"auth"`
}

View File

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

View File

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

View File

@ -7,6 +7,7 @@ type ServiceGroup struct {
HostService
BackupService
GroupService
ImageRepoService
ContainerService
CommandService
OperationService
@ -25,6 +26,7 @@ var (
commandRepo = repo.RepoGroupApp.CommandRepo
operationRepo = repo.RepoGroupApp.OperationRepo
commonRepo = repo.RepoGroupApp.CommonRepo
imageRepoRepo = repo.RepoGroupApp.ImageRepoRepo
cronjobRepo = repo.RepoGroupApp.CronjobRepo
settingRepo = repo.RepoGroupApp.SettingRepo
appRepo = repo.RepoGroupApp.AppRepo

View File

@ -0,0 +1,56 @@
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 ImageRepoService struct{}
type IImageRepoService interface {
Page(search dto.PageInfo) (int64, interface{}, error)
Create(imageRepoDto dto.ImageRepoCreate) error
Update(id uint, upMap map[string]interface{}) error
BatchDelete(ids []uint) error
}
func NewIImageRepoService() IImageRepoService {
return &ImageRepoService{}
}
func (u *ImageRepoService) Page(search dto.PageInfo) (int64, interface{}, error) {
total, ops, err := imageRepoRepo.Page(search.Page, search.PageSize, commonRepo.WithOrderBy("created_at desc"))
var dtoOps []dto.ImageRepoInfo
for _, op := range ops {
var item dto.ImageRepoInfo
if err := copier.Copy(&item, &op); err != nil {
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
}
dtoOps = append(dtoOps, item)
}
return total, dtoOps, err
}
func (u *ImageRepoService) Create(imageRepoDto dto.ImageRepoCreate) error {
imageRepo, _ := imageRepoRepo.Get(commonRepo.WithByName(imageRepoDto.RepoName))
if imageRepo.ID != 0 {
return constant.ErrRecordExist
}
if err := copier.Copy(&imageRepo, &imageRepoDto); err != nil {
return errors.WithMessage(constant.ErrStructTransform, err.Error())
}
if err := imageRepoRepo.Create(&imageRepo); err != nil {
return err
}
return nil
}
func (u *ImageRepoService) BatchDelete(ids []uint) error {
return imageRepoRepo.Delete(commonRepo.WithIdsIn(ids))
}
func (u *ImageRepoService) Update(id uint, upMap map[string]interface{}) error {
return imageRepoRepo.Update(id, upMap)
}

View File

@ -16,6 +16,7 @@ func Init() {
migrations.AddTableBackupAccount,
migrations.AddTableCronjob,
migrations.AddTableApp,
migrations.AddTableImageRepo,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View File

@ -153,3 +153,20 @@ var AddTableApp = &gormigrate.Migration{
return tx.AutoMigrate(&model.App{}, &model.AppDetail{}, &model.Tag{}, &model.AppTag{}, &model.AppInstall{}, &model.AppInstallResource{}, &model.Database{}, &model.AppInstallBackup{})
},
}
var AddTableImageRepo = &gormigrate.Migration{
ID: "20201009-add-table-imagerepo",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.ImageRepo{}); err != nil {
return err
}
item := &model.ImageRepo{
Name: "Docker Hub",
DownloadUrl: "docker.io",
}
if err := tx.Create(item).Error; err != nil {
return err
}
return nil
},
}

View File

@ -24,5 +24,10 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
baRouter.GET("/detail/:id", baseApi.ContainerDetail)
withRecordRouter.POST("operate", baseApi.ContainerOperation)
withRecordRouter.POST("/log", baseApi.ContainerLogs)
baRouter.POST("/repo/search", baseApi.GetRepoList)
baRouter.PUT("/repo/:id", baseApi.UpdateRepo)
withRecordRouter.POST("/repo", baseApi.CreateRepo)
withRecordRouter.POST("/repo/del", baseApi.DeleteRepo)
}
}

View File

@ -21,4 +21,30 @@ export namespace Container {
containerID: string;
mode: string;
}
export interface RepoCreate {
name: string;
downloadUrl: string;
repoName: string;
username: string;
password: string;
auth: boolean;
}
export interface RepoUpdate {
id: number;
downloadUrl: string;
username: string;
password: string;
auth: boolean;
}
export interface RepoInfo {
id: number;
createdAt: Date;
name: string;
downloadUrl: string;
repoName: string;
username: string;
password: string;
auth: boolean;
}
}

View File

@ -15,7 +15,6 @@ export const addCommand = (params: Command.CommandOperate) => {
};
export const editCommand = (params: Command.CommandOperate) => {
console.log(params.id);
return http.put(`/commands/${params.id}`, params);
};

View File

@ -1,5 +1,5 @@
import http from '@/api';
import { ResPage } from '../interface';
import { ResPage, ReqPage } from '../interface';
import { Container } from '../interface/container';
export const getContainerPage = (params: Container.ContainerSearch) => {
@ -17,3 +17,17 @@ export const ContainerOperator = (params: Container.ContainerOperate) => {
export const getContainerInspect = (containerID: string) => {
return http.get<string>(`/containers/detail/${containerID}`);
};
// repo
export const getRepoPage = (params: ReqPage) => {
return http.post<ResPage<Container.RepoInfo>>(`/containers/repo/search`, params);
};
export const repoCreate = (params: Container.RepoCreate) => {
return http.post(`/containers/repo`, params);
};
export const repoUpdate = (params: Container.RepoUpdate) => {
return http.put(`/containers/repo/${params.id}`, params);
};
export const deleteRepo = (params: { ids: number[] }) => {
return http.post(`/containers/repo/del`, params);
};

View File

@ -1,5 +1,7 @@
export default {
commons: {
true: 'true',
false: 'false',
button: {
create: 'Create',
add: 'Add',
@ -169,6 +171,13 @@ export default {
last4Hour: 'Last 4 Hours',
lastHour: 'Last Hour',
last10Min: 'Last 10 Minutes',
repo: 'Repo',
name: 'Name',
downloadUrl: 'Download URL',
imageRepo: 'ImageRepo',
repoHelper: 'Does it include a mirror repository/organization/project?',
auth: 'Auth',
},
cronjob: {
cronTask: 'Task',

View File

@ -1,5 +1,7 @@
export default {
commons: {
true: '是',
false: '否',
button: {
create: '新建',
add: '添加',
@ -166,6 +168,13 @@ export default {
last4Hour: '最近 4 小时',
lastHour: '最近 1 小时',
last10Min: '最近 10 分钟',
repo: '仓库',
name: '名称',
downloadUrl: '下载地址',
imageRepo: '镜像库',
repoHelper: '是否包含镜像仓库/组织/项目?',
auth: '认证',
},
cronjob: {
cronTask: '计划任务',

View File

@ -0,0 +1,204 @@
<template>
<el-dialog v-model="createVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="50%">
<template #header>
<div class="card-header">
<span>容器创建</span>
</div>
</template>
<el-form ref="formRef" :model="form" label-position="left" :rules="rules" label-width="120px">
<el-form-item label="容器名称" prop="name">
<el-input clearable v-model="form.name" />
</el-form-item>
<el-form-item label="镜像" prop="image">
<el-input clearable v-model="form.image" />
</el-form-item>
<el-form-item label="端口" prop="image">
<el-radio-group v-model="form.publishAllPorts" class="ml-4">
<el-radio :label="false">暴露端口</el-radio>
<el-radio :label="true">暴露所有</el-radio>
</el-radio-group>
<div style="margin-top: 20px"></div>
<table style="width: 100%; margin-top: 5px" class="tab-table">
<tr v-for="(row, index) in ports" :key="index">
<td width="48%">
<el-input v-model="row['key']" />
</td>
<td width="48%">
<el-input v-model="row['value']" />
</td>
<td>
<el-button type="text" style="font-size: 10px" @click="handlePortsDelete(index)">
{{ $t('commons.button.delete') }}
</el-button>
</td>
</tr>
<tr>
<td align="left">
<el-button @click="handlePortsAdd()">{{ $t('commons.button.add') }}</el-button>
</td>
</tr>
</table>
</el-form-item>
<el-form-item label="启动命令" prop="command">
<el-input clearable v-model="form.command" />
</el-form-item>
<el-form-item prop="autoRemove">
<el-checkbox v-model="form.autoRemove">容器停止后自动删除容器</el-checkbox>
</el-form-item>
<el-form-item label="限制CPU" prop="cpusetCpus">
<el-input v-model="form.cpusetCpus" />
</el-form-item>
<el-form-item label="内存" prop="memeryLimit">
<el-input v-model="form.memeryLimit" />
</el-form-item>
<el-form-item label="挂载卷">
<div style="margin-top: 20px"></div>
<table style="width: 100%; margin-top: 5px" class="tab-table">
<tr v-for="(row, index) in volumes" :key="index">
<td width="30%">
<el-input v-model="row['name']" />
</td>
<td width="30%">
<el-input v-model="row['bind']" />
</td>
<td width="30%">
<el-input v-model="row['mode']" />
</td>
<td>
<el-button type="text" style="font-size: 10px" @click="handleVolumesDelete(index)">
{{ $t('commons.button.delete') }}
</el-button>
</td>
</tr>
<tr>
<td align="left">
<el-button @click="handleVolumesAdd()">{{ $t('commons.button.add') }}</el-button>
</td>
</tr>
</table>
</el-form-item>
<el-form-item label="标签" prop="labels">
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" v-model="form.labels" />
</el-form-item>
<el-form-item label="环境变量(每行一个)" prop="environment">
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" v-model="form.environment" />
</el-form-item>
<el-form-item label="重启规则" prop="restartPolicy.value">
<el-radio-group v-model="form.restartPolicy.value">
<el-radio :label="false">关闭后马上重启</el-radio>
<el-radio :label="false">错误时重启默认重启 5 </el-radio>
<el-radio :label="true">不重启</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="createVisiable = 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 { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { ElForm, ElMessage } from 'element-plus';
const createVisiable = ref(false);
const form = reactive({
name: '',
image: '',
command: '',
publishAllPorts: false,
ports: [],
cpusetCpus: 1,
memeryLimit: 100,
volumes: [],
autoRemove: false,
labels: '',
environment: '',
restartPolicy: {
value: '',
name: '',
maximumRetryCount: '',
},
});
const ports = ref();
const volumes = ref();
const acceptParams = (): void => {
createVisiable.value = true;
};
const emit = defineEmits<{ (e: 'search'): void }>();
const rules = reactive({
name: [Rules.requiredInput, Rules.name],
type: [Rules.requiredSelect],
specType: [Rules.requiredSelect],
week: [Rules.requiredSelect, Rules.number],
day: [Rules.number, { max: 31, min: 1 }],
hour: [Rules.number, { max: 23, min: 0 }],
minute: [Rules.number, { max: 60, min: 1 }],
script: [Rules.requiredInput],
website: [Rules.requiredSelect],
database: [Rules.requiredSelect],
url: [Rules.requiredInput],
sourceDir: [Rules.requiredSelect],
targetDirID: [Rules.requiredSelect, Rules.number],
retainCopies: [Rules.number],
});
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const handlePortsAdd = () => {
let item = {
key: '',
value: '',
};
ports.value.push(item);
};
const handlePortsDelete = (index: number) => {
ports.value.splice(index, 1);
};
const handleVolumesAdd = () => {
let item = {
from: '',
bind: '',
mode: '',
};
volumes.value.push(item);
};
const handleVolumesDelete = (index: number) => {
volumes.value.splice(index, 1);
};
function restForm() {
if (formRef.value) {
formRef.value.resetFields();
}
}
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
restForm();
emit('search');
createVisiable.value = false;
});
};
defineExpose({
acceptParams,
});
</script>

View File

@ -1,51 +1,65 @@
<template>
<el-card style="margin-top: 20px">
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
<template #toolbar>
<el-button-group>
<el-button :disabled="checkStatus('start')" @click="onOperate('start')">
{{ $t('container.start') }}
<div>
<el-card style="margin-top: 20px">
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
<template #toolbar>
<el-button-group>
<el-button :disabled="checkStatus('start')" @click="onOperate('start')">
{{ $t('container.start') }}
</el-button>
<el-button :disabled="checkStatus('stop')" @click="onOperate('stop')">
{{ $t('container.stop') }}
</el-button>
<el-button :disabled="checkStatus('reStart')" @click="onOperate('reStart')">
{{ $t('container.reStart') }}
</el-button>
<el-button :disabled="checkStatus('kill')" @click="onOperate('kill')">
{{ $t('container.kill') }}
</el-button>
<el-button :disabled="checkStatus('pause')" @click="onOperate('pause')">
{{ $t('container.pause') }}
</el-button>
<el-button :disabled="checkStatus('unPause')" @click="onOperate('unPause')">
{{ $t('container.unPause') }}
</el-button>
<el-button :disabled="checkStatus('remove')" @click="onOperate('remove')">
{{ $t('container.remove') }}
</el-button>
</el-button-group>
<el-button icon="Plus" style="margin-left: 10px" @click="onCreate()">
{{ $t('commons.button.create') }}
</el-button>
<el-button :disabled="checkStatus('stop')" @click="onOperate('stop')">
{{ $t('container.stop') }}
</el-button>
<el-button :disabled="checkStatus('reStart')" @click="onOperate('reStart')">
{{ $t('container.reStart') }}
</el-button>
<el-button :disabled="checkStatus('kill')" @click="onOperate('kill')">
{{ $t('container.kill') }}
</el-button>
<el-button :disabled="checkStatus('pause')" @click="onOperate('pause')">
{{ $t('container.pause') }}
</el-button>
<el-button :disabled="checkStatus('unPause')" @click="onOperate('unPause')">
{{ $t('container.unPause') }}
</el-button>
<el-button :disabled="checkStatus('remove')" @click="onOperate('remove')">
{{ $t('container.remove') }}
</el-button>
</el-button-group>
<el-button icon="Plus" style="margin-left: 10px" @click="onCreate()">
{{ $t('commons.button.create') }}
</el-button>
</template>
<el-table-column type="selection" fix />
<el-table-column :label="$t('commons.table.name')" show-overflow-tooltip min-width="100" prop="name" fix />
<el-table-column :label="$t('container.image')" show-overflow-tooltip min-width="100" prop="imageName" />
<el-table-column :label="$t('commons.table.status')" min-width="50" prop="state" fix />
<el-table-column :label="$t('container.upTime')" min-width="100" prop="runTime" fix />
<el-table-column
prop="createTime"
:label="$t('commons.table.date')"
:formatter="dateFromat"
show-overflow-tooltip
/>
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" fix />
</ComplexTable>
</template>
<el-table-column type="selection" fix />
<el-table-column
:label="$t('commons.table.name')"
show-overflow-tooltip
min-width="100"
prop="name"
fix
/>
<el-table-column
:label="$t('container.image')"
show-overflow-tooltip
min-width="100"
prop="imageName"
/>
<el-table-column :label="$t('commons.table.status')" min-width="50" prop="state" fix />
<el-table-column :label="$t('container.upTime')" min-width="100" prop="runTime" fix />
<el-table-column
prop="createTime"
:label="$t('commons.table.date')"
:formatter="dateFromat"
show-overflow-tooltip
/>
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" fix />
</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.views') }}</span>
<span>{{ $t('commons.button.view') }}</span>
</div>
</template>
<codemirror
@ -138,11 +152,13 @@
</span>
</template>
</el-dialog>
</el-card>
<CreateDialog ref="dialogCreateRef" />
</div>
</template>
<script lang="ts" setup>
import ComplexTable from '@/components/complex-table/index.vue';
import CreateDialog from '@/views/container/container/create/index.vue';
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
@ -158,7 +174,7 @@ const data = ref();
const selects = ref<any>([]);
const paginationConfig = reactive({
page: 1,
pageSize: 100,
pageSize: 10,
total: 0,
});
const containerSearch = reactive({
@ -219,7 +235,15 @@ const search = async () => {
});
};
const onCreate = async () => {};
const dialogCreateRef = ref<DialogExpose>();
interface DialogExpose {
acceptParams: () => void;
}
const onCreate = async () => {
dialogCreateRef.value!.acceptParams();
};
const onDetail = async (row: Container.ContainerInfo) => {
const res = await getContainerInspect(row.containerID);
detailInfo.value = JSON.stringify(JSON.parse(res.data), null, 2);

View File

@ -14,13 +14,16 @@
<el-radio-button class="topButton" size="large" label="storage">
{{ $t('container.storage') }}
</el-radio-button>
<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>
</el-radio-group>
</el-card>
<Container v-if="activeNames === 'container'" />
<Safe v-if="activeNames === 'image'" />
<Repo v-if="activeNames === 'repo'" />
<Backup v-if="activeNames === 'network'" />
<Monitor v-if="activeNames === 'storage'" />
<About v-if="activeNames === 'schedule'" />
@ -30,7 +33,7 @@
<script lang="ts" setup>
import { ref } from 'vue';
import Container from '@/views/container/container/index.vue';
import Safe from '@/views/setting/tabs/safe.vue';
import Repo from '@/views/container/repo/index.vue';
import Backup from '@/views/setting/tabs/backup.vue';
import Monitor from '@/views/setting/tabs/monitor.vue';
import About from '@/views/setting/tabs/about.vue';

View File

@ -0,0 +1,127 @@
<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')" prop="name" min-width="60" />
<el-table-column
:label="$t('container.downloadUrl')"
show-overflow-tooltip
prop="downloadUrl"
min-width="100"
fix
/>
<el-table-column
:label="$t('container.imageRepo')"
show-overflow-tooltip
prop="repoName"
min-width="70"
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>
<OperatorDialog @search="search" ref="dialogRef" />
</div>
</template>
<script lang="ts" setup>
import ComplexTable from '@/components/complex-table/index.vue';
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 { useDeleteData } from '@/hooks/use-delete-data';
import i18n from '@/lang';
const data = ref();
const selects = ref<any>([]);
const paginationConfig = reactive({
page: 1,
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) => {
if (res.data) {
data.value = res.data.items;
}
});
};
interface DialogExpose {
acceptParams: (params: any) => void;
}
const dialogRef = ref<DialogExpose>();
const onOpenDialog = async (
title: string,
rowData: Partial<Container.RepoInfo> = {
auth: true,
},
) => {
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(deleteRepo, { 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,107 @@
<template>
<el-dialog v-model="repoVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="50%">
<template #header>
<div class="card-header">
<span>{{ title }}{{ $t('container.repo') }}</span>
</div>
</template>
<el-form ref="formRef" :model="dialogData.rowData" label-position="left" :rules="rules" label-width="120px">
<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.auth')" prop="auth">
<el-radio-group v-model="dialogData.rowData!.auth">
<el-radio :label="true">{{ $t('commons.true') }}</el-radio>
<el-radio :label="false">{{ $t('commons.false') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="dialogData.rowData!.auth" :label="$t('auth.username')" prop="username">
<el-input v-model="dialogData.rowData!.username"></el-input>
</el-form-item>
<el-form-item v-if="dialogData.rowData!.auth" :label="$t('auth.password')" prop="password">
<el-input type="password" v-model="dialogData.rowData!.password"></el-input>
</el-form-item>
<el-form-item :label="$t('container.downloadUrl')" prop="downloadUrl">
<el-input v-model="dialogData.rowData!.downloadUrl" :placeholder="'172.16.10.10:8081'"></el-input>
</el-form-item>
<el-form-item :label="$t('container.imageRepo')" prop="repoName">
<el-checkbox v-model="hasRepo">{{ $t('container.repoHelper') }}</el-checkbox>
<el-input v-if="hasRepo" v-model="dialogData.rowData!.repoName"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="repoVisiable = 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 { 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';
interface DialogProps {
title: string;
rowData?: Container.RepoInfo;
getTableList?: () => Promise<any>;
}
const title = ref<string>('');
const repoVisiable = 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);
hasRepo.value = params.rowData?.repoName ? params.rowData?.repoName.length !== 0 : false;
repoVisiable.value = true;
};
const emit = defineEmits<{ (e: 'search'): void }>();
const hasRepo = ref(false);
const rules = reactive({
name: [Rules.requiredInput, Rules.name],
downloadUrl: [Rules.requiredInput],
repoName: [Rules.requiredInput],
username: [Rules.requiredInput],
password: [Rules.requiredInput],
auth: [Rules.requiredSelect],
});
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 repoCreate(dialogData.value.rowData!);
}
if (dialogData.value.title === 'edit') {
await repoUpdate(dialogData.value.rowData!);
}
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
restForm();
emit('search');
repoVisiable.value = false;
});
};
defineExpose({
acceptParams,
});
</script>