mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2024-12-18 11:17:49 +08:00
feat: 完成镜像仓库管理
This commit is contained in:
parent
a79ba71ef4
commit
dcbf92ac12
@ -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
|
||||
|
92
backend/app/api/v1/image_repo.go
Normal file
92
backend/app/api/v1/image_repo.go
Normal 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)
|
||||
}
|
31
backend/app/dto/image_repo.go
Normal file
31
backend/app/dto/image_repo.go
Normal 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"`
|
||||
}
|
12
backend/app/model/image_repo.go
Normal file
12
backend/app/model/image_repo.go
Normal 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"`
|
||||
}
|
@ -4,6 +4,7 @@ type RepoGroup struct {
|
||||
HostRepo
|
||||
BackupRepo
|
||||
GroupRepo
|
||||
ImageRepoRepo
|
||||
CommandRepo
|
||||
OperationRepo
|
||||
CommonRepo
|
||||
|
58
backend/app/repo/image_repo.go
Normal file
58
backend/app/repo/image_repo.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
56
backend/app/service/image_repo.go
Normal file
56
backend/app/service/image_repo.go
Normal 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)
|
||||
}
|
@ -16,6 +16,7 @@ func Init() {
|
||||
migrations.AddTableBackupAccount,
|
||||
migrations.AddTableCronjob,
|
||||
migrations.AddTableApp,
|
||||
migrations.AddTableImageRepo,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
@ -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
|
||||
},
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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',
|
||||
|
@ -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: '计划任务',
|
||||
|
204
frontend/src/views/container/container/create/index.vue
Normal file
204
frontend/src/views/container/container/create/index.vue
Normal 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>
|
@ -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);
|
||||
|
@ -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';
|
||||
|
127
frontend/src/views/container/repo/index.vue
Normal file
127
frontend/src/views/container/repo/index.vue
Normal 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>
|
107
frontend/src/views/container/repo/operator/index.vue
Normal file
107
frontend/src/views/container/repo/operator/index.vue
Normal 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>
|
Loading…
Reference in New Issue
Block a user