feat: 增加容器状态统计及状态快速切换 (#6594)

This commit is contained in:
ssongliu 2024-09-26 22:01:32 +08:00 committed by GitHub
parent 1e1fce4c77
commit c101dfb694
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 634 additions and 448 deletions

View File

@ -54,6 +54,24 @@ func (b *BaseApi) ListContainer(c *gin.Context) {
helper.SuccessWithData(c, list)
}
// @Tags Container
// @Summary Load containers status
// @Description 获取容器状态
// @Accept json
// @Produce json
// @Success 200
// @Security ApiKeyAuth
// @Router /containers/status [get]
func (b *BaseApi) LoadContainerStatus(c *gin.Context) {
data, err := containerService.LoadStatus()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Container Compose
// @Summary Page composes
// @Description 获取编排列表分页

View File

@ -8,7 +8,7 @@ type PageContainer struct {
PageInfo
Name string `json:"name"`
State string `json:"state" validate:"required,oneof=all created running paused restarting removing exited dead"`
OrderBy string `json:"orderBy" validate:"required,oneof=name state created_at"`
OrderBy string `json:"orderBy" validate:"required,oneof=name created_at"`
Order string `json:"order" validate:"required,oneof=null ascending descending"`
Filters string `json:"filters"`
ExcludeAppStore bool `json:"excludeAppStore"`
@ -39,6 +39,16 @@ type ContainerInfo struct {
Websites []string `json:"websites"`
}
type ContainerStatus struct {
All uint `json:"all"`
Created uint `json:"created"`
Running uint `json:"running"`
Paused uint `json:"paused"`
Restarting uint `json:"restarting"`
Removing uint `json:"removing"`
Exited uint `json:"exited"`
Dead uint `json:"dead"`
}
type ResourceLimit struct {
CPU int `json:"cpu"`
Memory uint64 `json:"memory"`

View File

@ -51,6 +51,7 @@ type ContainerService struct{}
type IContainerService interface {
Page(req dto.PageContainer) (int64, interface{}, error)
List() ([]string, error)
LoadStatus() (dto.ContainerStatus, error)
PageNetwork(req dto.SearchWithPage) (int64, interface{}, error)
ListNetwork() ([]dto.Options, error)
PageVolume(req dto.SearchWithPage) (int64, interface{}, error)
@ -149,13 +150,6 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro
}
return list[i].Names[0][1:] > list[j].Names[0][1:]
})
case "state":
sort.Slice(list, func(i, j int) bool {
if req.Order == constant.OrderAsc {
return list[i].State < list[j].State
}
return list[i].State > list[j].State
})
default:
sort.Slice(list, func(i, j int) bool {
if req.Order == constant.OrderAsc {
@ -245,6 +239,38 @@ func (u *ContainerService) List() ([]string, error) {
return datas, nil
}
func (u *ContainerService) LoadStatus() (dto.ContainerStatus, error) {
var data dto.ContainerStatus
client, err := docker.NewDockerClient()
if err != nil {
return data, err
}
defer client.Close()
containers, err := client.ContainerList(context.Background(), container.ListOptions{All: true})
if err != nil {
return data, err
}
data.All = uint(len(containers))
for _, item := range containers {
switch item.State {
case "created":
data.Created++
case "running":
data.Running++
case "paused":
data.Paused++
case "restarting":
data.Restarting++
case "dead":
data.Dead++
case "exited":
data.Exited++
case "removing":
data.Removing++
}
}
return data, nil
}
func (u *ContainerService) ContainerListStats() ([]dto.ContainerListStats, error) {
client, err := docker.NewDockerClient()
if err != nil {

View File

@ -20,6 +20,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
baRouter.POST("/info", baseApi.ContainerInfo)
baRouter.POST("/search", baseApi.SearchContainer)
baRouter.POST("/list", baseApi.ListContainer)
baRouter.GET("/status", baseApi.LoadContainerStatus)
baRouter.GET("/list/stats", baseApi.ContainerListStats)
baRouter.GET("/search/log", baseApi.ContainerLogs)
baRouter.POST("/download/log", baseApi.DownloadContainerLogs)

View File

@ -24,6 +24,16 @@ export namespace Container {
orderBy: string;
order: string;
}
export interface ContainerStatus {
all: number;
created: number;
running: number;
paused: number;
restarting: number;
removing: number;
exited: number;
dead: number;
}
export interface ResourceLimit {
cpu: number;
memory: number;

View File

@ -9,6 +9,9 @@ export const searchContainer = (params: Container.ContainerSearch) => {
export const listContainer = () => {
return http.post<Array<string>>(`/containers/list`, {});
};
export const loadContainerStatus = () => {
return http.get<Container.ContainerStatus>(`/containers/status`);
};
export const loadResourceLimit = () => {
return http.get<Container.ResourceLimit>(`/containers/limit`);
};

View File

@ -318,7 +318,7 @@ const message = {
host: '主机',
files: '文件',
monitor: '监控',
terminal: '终端',
terminal: 'WEB终端',
settings: '面板设置',
toolbox: '工具箱',
logs: '日志审计',
@ -1137,7 +1137,7 @@ const message = {
role: '权限',
info: '属性',
linkFile: '软连接文件',
terminal: 'Web终端',
terminal: '终端',
shareList: '分享列表',
zip: '压缩',
group: '用户组',

View File

@ -28,6 +28,17 @@ const containerRouter = {
requiresAuth: false,
},
},
{
path: 'container/operate',
name: 'ContainerCreate',
component: () => import('@/views/container/container/operate/index.vue'),
props: true,
hidden: true,
meta: {
activeMenu: '/containers',
requiresAuth: false,
},
},
{
path: 'composeDetail/:filters?',
name: 'ComposeDetail',

View File

@ -5,9 +5,73 @@
<el-button type="primary" class="bt" link @click="goSetting"> {{ $t('container.setting') }} </el-button>
<span>{{ $t('container.startIn') }}</span>
</el-card>
<div class="mt-5">
<el-tag @click="searchWithStatus('all')" v-if="countItem.all" effect="plain" size="large">
{{ $t('commons.table.all') }} * {{ countItem.all }}
</el-tag>
<el-tag
@click="searchWithStatus('running')"
v-if="countItem.running"
effect="plain"
size="large"
class="ml-2"
>
{{ $t('commons.status.running') }} * {{ countItem.running }}
</el-tag>
<el-tag
@click="searchWithStatus('created')"
v-if="countItem.created"
effect="plain"
size="large"
class="ml-2"
>
{{ $t('commons.status.created') }} * {{ countItem.created }}
</el-tag>
<el-tag
@click="searchWithStatus('paused')"
v-if="countItem.paused"
effect="plain"
size="large"
class="ml-2"
>
{{ $t('commons.status.paused') }} * {{ countItem.paused }}
</el-tag>
<el-tag
@click="searchWithStatus('restarting')"
v-if="countItem.restarting"
effect="plain"
size="large"
class="ml-2"
>
{{ $t('commons.status.restarting') }} * {{ countItem.restarting }}
</el-tag>
<el-tag
@click="searchWithStatus('removing')"
v-if="countItem.removing"
effect="plain"
size="large"
class="ml-2"
>
{{ $t('commons.status.removing') }} * {{ countItem.removing }}
</el-tag>
<el-tag
@click="searchWithStatus('exited')"
v-if="countItem.exited"
effect="plain"
size="large"
class="ml-2"
>
{{ $t('commons.status.exited') }} * {{ countItem.exited }}
</el-tag>
<el-tag @click="searchWithStatus('dead')" v-if="countItem.dead" effect="plain" size="large" class="ml-2">
{{ $t('commons.status.dead') }} * {{ countItem.dead }}
</el-tag>
</div>
<LayoutContent :title="$t('container.container')" :class="{ mask: dockerStatus != 'Running' }">
<template #leftToolBar>
<el-button type="primary" @click="onOpenDialog('create')">
<el-button type="primary" @click="onContainerOperate('')">
{{ $t('container.create') }}
</el-button>
<el-button type="primary" plain @click="onClean()">
@ -41,17 +105,6 @@
<el-checkbox v-model="includeAppStore" @change="search()" class="!mr-2.5">
{{ $t('container.includeAppstore') }}
</el-checkbox>
<el-select v-model="searchState" @change="search()" clearable class="p-w-200 mr-2.5">
<template #prefix>{{ $t('commons.table.status') }}</template>
<el-option :label="$t('commons.table.all')" value="all"></el-option>
<el-option :label="$t('commons.status.created')" value="created"></el-option>
<el-option :label="$t('commons.status.running')" value="running"></el-option>
<el-option :label="$t('commons.status.paused')" value="paused"></el-option>
<el-option :label="$t('commons.status.restarting')" value="restarting"></el-option>
<el-option :label="$t('commons.status.removing')" value="removing"></el-option>
<el-option :label="$t('commons.status.exited')" value="exited"></el-option>
<el-option :label="$t('commons.status.dead')" value="dead"></el-option>
</el-select>
<TableSearch @search="search()" v-model:searchName="searchName" class="mr-2.5" />
<TableSetting title="container-refresh" @search="refresh()" class="mr-2.5" />
<fu-table-column-select
@ -98,9 +151,51 @@
min-width="150"
prop="imageName"
/>
<el-table-column :label="$t('commons.table.status')" min-width="100" prop="state" sortable>
<el-table-column :label="$t('commons.table.status')" min-width="100" prop="state">
<template #default="{ row }">
<el-dropdown placement="bottom">
<Status :key="row.state" :status="row.state"></Status>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
:disabled="checkStatus('start', row)"
@click="onOperate('start', row)"
>
{{ $t('container.start') }}
</el-dropdown-item>
<el-dropdown-item
:disabled="checkStatus('stop', row)"
@click="onOperate('stop', row)"
>
{{ $t('container.stop') }}
</el-dropdown-item>
<el-dropdown-item
:disabled="checkStatus('restart', row)"
@click="onOperate('restart', row)"
>
{{ $t('container.restart') }}
</el-dropdown-item>
<el-dropdown-item
:disabled="checkStatus('kill', row)"
@click="onOperate('kill', row)"
>
{{ $t('container.kill') }}
</el-dropdown-item>
<el-dropdown-item
:disabled="checkStatus('pause', row)"
@click="onOperate('pause', row)"
>
{{ $t('container.pause') }}
</el-dropdown-item>
<el-dropdown-item
:disabled="checkStatus('unpause', row)"
@click="onOperate('unpause', row)"
>
{{ $t('container.unpause') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
<el-table-column
@ -299,7 +394,6 @@
<RenameDialog @search="search" ref="dialogRenameRef" />
<ContainerLogDialog ref="dialogContainerLogRef" />
<OperateDialog @search="search" ref="dialogOperateRef" />
<UpgradeDialog @search="search" ref="dialogUpgradeRef" />
<CommitDialog @search="search" ref="dialogCommitRef" />
<MonitorDialog ref="dialogMonitorRef" />
@ -312,7 +406,6 @@
<script lang="ts" setup>
import PruneDialog from '@/views/container/container/prune/index.vue';
import RenameDialog from '@/views/container/container/rename/index.vue';
import OperateDialog from '@/views/container/container/operate/index.vue';
import UpgradeDialog from '@/views/container/container/upgrade/index.vue';
import CommitDialog from '@/views/container/container/commit/index.vue';
import MonitorDialog from '@/views/container/container/monitor/index.vue';
@ -326,7 +419,7 @@ import {
containerListStats,
containerOperator,
inspect,
loadContainerInfo,
loadContainerStatus,
loadDockerStatus,
searchContainer,
} from '@/api/modules/container';
@ -349,11 +442,11 @@ const paginationConfig = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
state: 'all',
orderBy: 'created_at',
order: 'null',
});
const searchName = ref();
const searchState = ref('all');
const dialogUpgradeRef = ref();
const dialogCommitRef = ref();
const dialogPortJumpRef = ref();
@ -361,6 +454,17 @@ const opRef = ref();
const includeAppStore = ref(true);
const columns = ref([]);
const countItem = reactive({
all: 0,
created: 0,
running: 0,
paused: 0,
restarting: 0,
removing: 0,
exited: 0,
dead: 0,
});
const dockerStatus = ref('Running');
const loadStatus = async () => {
loading.value = true;
@ -417,7 +521,7 @@ const search = async (column?: any) => {
paginationConfig.order = column?.order ? column.order : paginationConfig.order;
let params = {
name: searchName.value,
state: searchState.value || 'all',
state: paginationConfig.state || 'all',
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
filters: filterItem,
@ -427,6 +531,7 @@ const search = async (column?: any) => {
};
loading.value = true;
loadStats();
loadContainerCount();
await searchContainer(params)
.then((res) => {
loading.value = false;
@ -438,11 +543,29 @@ const search = async (column?: any) => {
});
};
const searchWithStatus = (item: any) => {
paginationConfig.state = item;
search();
};
const loadContainerCount = async () => {
await loadContainerStatus().then((res) => {
countItem.all = res.data.all;
countItem.running = res.data.running;
countItem.paused = res.data.paused;
countItem.restarting = res.data.restarting;
countItem.removing = res.data.removing;
countItem.created = res.data.created;
countItem.dead = res.data.dead;
countItem.exited = res.data.exited;
});
};
const refresh = async () => {
let filterItem = props.filters ? props.filters : '';
let params = {
name: searchName.value,
state: searchState.value || 'all',
state: paginationConfig.state || 'all',
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
filters: filterItem,
@ -522,35 +645,8 @@ function loadMemValue(t: number) {
return Number((t / Math.pow(num, 3)).toFixed(2));
}
const dialogOperateRef = ref();
const onEdit = async (container: string) => {
const res = await loadContainerInfo(container);
if (res.data) {
onOpenDialog('edit', res.data);
}
};
const onOpenDialog = async (
title: string,
rowData: Partial<Container.ContainerHelper> = {
cmd: [],
cmdStr: '',
publishAllPorts: false,
exposedPorts: [],
cpuShares: 1024,
nanoCPUs: 0,
memory: 0,
memoryItem: 0,
volumes: [],
labels: [],
env: [],
restartPolicy: 'no',
},
) => {
let params = {
title,
rowData: { ...rowData },
};
dialogOperateRef.value!.acceptParams(params);
const onContainerOperate = async (containerID: string) => {
router.push({ name: 'ContainerCreate', query: { containerID: containerID } });
};
const dialogMonitorRef = ref();
@ -654,7 +750,7 @@ const buttons = [
{
label: i18n.global.t('commons.button.edit'),
click: (row: Container.ContainerInfo) => {
onEdit(row.containerID);
onContainerOperate(row.containerID);
},
},
{
@ -690,60 +786,6 @@ const buttons = [
return checkStatus('commit', row);
},
},
{
label: i18n.global.t('container.start'),
click: (row: Container.ContainerInfo) => {
onOperate('start', row);
},
disabled: (row: any) => {
return checkStatus('start', row);
},
},
{
label: i18n.global.t('container.stop'),
click: (row: Container.ContainerInfo) => {
onOperate('stop', row);
},
disabled: (row: any) => {
return checkStatus('stop', row);
},
},
{
label: i18n.global.t('container.restart'),
click: (row: Container.ContainerInfo) => {
onOperate('restart', row);
},
disabled: (row: any) => {
return checkStatus('restart', row);
},
},
{
label: i18n.global.t('container.kill'),
click: (row: Container.ContainerInfo) => {
onOperate('kill', row);
},
disabled: (row: any) => {
return checkStatus('kill', row);
},
},
{
label: i18n.global.t('container.pause'),
click: (row: Container.ContainerInfo) => {
onOperate('pause', row);
},
disabled: (row: any) => {
return checkStatus('pause', row);
},
},
{
label: i18n.global.t('container.unpause'),
click: (row: Container.ContainerInfo) => {
onOperate('unpause', row);
},
disabled: (row: any) => {
return checkStatus('unpause', row);
},
},
{
label: i18n.global.t('container.remove'),
click: (row: Container.ContainerInfo) => {

View File

@ -1,35 +1,38 @@
<template>
<DrawerPro
v-model="drawerVisible"
:header="title"
:back="handleClose"
:resource="dialogData.title === 'create' ? '' : dialogData.rowData?.name"
size="large"
>
<el-form
ref="formRef"
label-position="top"
v-loading="loading"
:model="dialogData.rowData!"
:rules="rules"
label-width="80px"
>
<div>
<LayoutContent :title="isCreate ? $t('container.create') : $t('commons.button.edit') + ' - ' + form.name">
<template #prompt>
<el-alert
v-if="dialogData.title === 'edit' && isFromApp(dialogData.rowData!)"
v-if="!isCreate && isFromApp(form)"
:title="$t('container.containerFromAppHelper')"
:closable="false"
type="error"
/>
</template>
<template #main>
<el-form
ref="formRef"
label-position="top"
v-loading="loading"
:model="form"
:rules="rules"
label-width="80px"
>
<el-row>
<el-col :span="1"><br /></el-col>
<el-col :xs="24" :sm="20" :md="15" :lg="12" :xl="12">
<el-form-item class="mt-5" :label="$t('commons.table.name')" prop="name">
<el-input
:disabled="isFromApp(dialogData.rowData!)"
clearable
v-model.trim="dialogData.rowData!.name"
/>
<div v-if="dialogData.title === 'edit' && isFromApp(dialogData.rowData!)">
<el-input :disabled="isFromApp(form)" clearable v-model.trim="form.name" />
<div v-if="!isCreate && isFromApp(form)">
<span class="input-help">
{{ $t('container.containerFromAppHelper1') }}
<el-button style="margin-left: -5px" size="small" text type="primary" @click="goRouter()">
<el-button
style="margin-left: -5px"
size="small"
text
type="primary"
@click="goRouter()"
>
<el-icon><Position /></el-icon>
{{ $t('firewall.quickJump') }}
</el-button>
@ -37,38 +40,46 @@
</div>
</el-form-item>
<el-form-item :label="$t('container.image')" prop="image">
<el-checkbox v-model="dialogData.rowData!.imageInput" :label="$t('container.input')" />
<el-select v-if="!dialogData.rowData!.imageInput" filterable v-model="dialogData.rowData!.image">
<el-option v-for="(item, index) of images" :key="index" :value="item.option" :label="item.option" />
<el-checkbox v-model="form.imageInput" :label="$t('container.input')" />
<el-select v-if="!form.imageInput" filterable v-model="form.image">
<el-option
v-for="(item, index) of images"
:key="index"
:value="item.option"
:label="item.option"
/>
</el-select>
<el-input v-else v-model="dialogData.rowData!.image" />
<el-input v-else v-model="form.image" />
</el-form-item>
<el-form-item prop="forcePull">
<el-checkbox v-model="dialogData.rowData!.forcePull">
<el-checkbox v-model="form.forcePull">
{{ $t('container.forcePull') }}
</el-checkbox>
<span class="input-help">{{ $t('container.forcePullHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('commons.table.port')">
<el-radio-group v-model="dialogData.rowData!.publishAllPorts" class="ml-4">
<el-radio-group v-model="form.publishAllPorts" class="ml-4">
<el-radio :value="false">{{ $t('container.exposePort') }}</el-radio>
<el-radio :value="true">{{ $t('container.exposeAll') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="!dialogData.rowData!.publishAllPorts">
<el-form-item v-if="!form.publishAllPorts">
<el-card class="widthClass">
<el-table
v-if="dialogData.rowData!.exposedPorts.length !== 0"
:data="dialogData.rowData!.exposedPorts"
>
<el-table v-if="form.exposedPorts.length !== 0" :data="form.exposedPorts">
<el-table-column :label="$t('container.server')" min-width="150">
<template #default="{ row }">
<el-input :placeholder="$t('container.serverExample')" v-model="row.host" />
<el-input
:placeholder="$t('container.serverExample')"
v-model="row.host"
/>
</template>
</el-table-column>
<el-table-column :label="$t('container.container')" min-width="80">
<template #default="{ row }">
<el-input :placeholder="$t('container.containerExample')" v-model="row.containerPort" />
<el-input
:placeholder="$t('container.containerExample')"
v-model="row.containerPort"
/>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.protocol')" min-width="50">
@ -98,7 +109,7 @@
</el-card>
</el-form-item>
<el-form-item :label="$t('container.network')" prop="network">
<el-select v-model="dialogData.rowData!.network">
<el-select v-model="form.network">
<el-option
v-for="(item, indexV) of networks"
:key="indexV"
@ -109,29 +120,48 @@
</el-form-item>
<el-form-item label="ipv4" prop="ipv4">
<el-input v-model="dialogData.rowData!.ipv4" :placeholder="$t('container.inputIpv4')" />
<el-input v-model="form.ipv4" :placeholder="$t('container.inputIpv4')" />
</el-form-item>
<el-form-item label="ipv6" prop="ipv6">
<el-input v-model="dialogData.rowData!.ipv6" :placeholder="$t('container.inputIpv6')" />
<el-input v-model="form.ipv6" :placeholder="$t('container.inputIpv6')" />
</el-form-item>
<el-form-item :label="$t('container.mount')">
<div v-for="(row, index) in dialogData.rowData!.volumes" :key="index" style="width: 100%">
<div v-for="(row, index) in form.volumes" :key="index" style="width: 100%">
<el-card class="mt-1">
<el-radio-group v-model="row.type">
<el-radio-button value="volume">{{ $t('container.volumeOption') }}</el-radio-button>
<el-radio-button value="bind">{{ $t('container.hostOption') }}</el-radio-button>
<el-radio-button value="volume">
{{ $t('container.volumeOption') }}
</el-radio-button>
<el-radio-button value="bind">
{{ $t('container.hostOption') }}
</el-radio-button>
</el-radio-group>
<el-button class="float-right mt-3" link type="primary" @click="handleVolumesDelete(index)">
<el-button
class="float-right mt-3"
link
type="primary"
@click="handleVolumesDelete(index)"
>
{{ $t('commons.button.delete') }}
</el-button>
<el-row class="mt-4" :gutter="5">
<el-col :span="10">
<el-form-item v-if="row.type === 'volume'" :label="$t('container.volumeOption')">
<el-form-item
v-if="row.type === 'volume'"
:label="$t('container.volumeOption')"
>
<el-select filterable v-model="row.sourceDir">
<div v-for="(item, indexV) of volumes" :key="indexV">
<el-tooltip :hide-after="20" :content="item.option" placement="top">
<el-option :value="item.option" :label="item.option.substring(0, 30)" />
<el-tooltip
:hide-after="20"
:content="item.option"
placement="top"
>
<el-option
:value="item.option"
:label="item.option.substring(0, 30)"
/>
</el-tooltip>
</div>
</el-select>
@ -161,30 +191,33 @@
</el-button>
</el-form-item>
<el-form-item label="Command" prop="cmdStr">
<el-input v-model="dialogData.rowData!.cmdStr" :placeholder="$t('container.cmdHelper')" />
<el-input v-model="form.cmdStr" :placeholder="$t('container.cmdHelper')" />
</el-form-item>
<el-form-item label="Entrypoint" prop="entrypointStr">
<el-input v-model="dialogData.rowData!.entrypointStr" :placeholder="$t('container.entrypointHelper')" />
<el-input
v-model="form.entrypointStr"
:placeholder="$t('container.entrypointHelper')"
/>
</el-form-item>
<el-form-item prop="autoRemove">
<el-checkbox v-model="dialogData.rowData!.autoRemove">
<el-checkbox v-model="form.autoRemove">
{{ $t('container.autoRemove') }}
</el-checkbox>
</el-form-item>
<el-form-item>
<el-checkbox v-model="dialogData.rowData!.privileged">
<el-checkbox v-model="form.privileged">
{{ $t('container.privileged') }}
</el-checkbox>
<span class="input-help">{{ $t('container.privilegedHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('container.console')">
<el-checkbox v-model="dialogData.rowData!.tty">{{ $t('container.tty') }}</el-checkbox>
<el-checkbox v-model="dialogData.rowData!.openStdin">
<el-checkbox v-model="form.tty">{{ $t('container.tty') }}</el-checkbox>
<el-checkbox v-model="form.openStdin">
{{ $t('container.openStdin') }}
</el-checkbox>
</el-form-item>
<el-form-item :label="$t('container.restartPolicy')" prop="restartPolicy">
<el-radio-group v-model="dialogData.rowData!.restartPolicy">
<el-radio-group v-model="form.restartPolicy">
<el-radio value="no">{{ $t('container.no') }}</el-radio>
<el-radio value="always">{{ $t('container.always') }}</el-radio>
<el-radio value="on-failure">{{ $t('container.onFailure') }}</el-radio>
@ -192,7 +225,7 @@
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('container.cpuShare')" prop="cpuShares">
<el-input class="mini-form-item" v-model.number="dialogData.rowData!.cpuShares" />
<el-input class="mini-form-item" v-model.number="form.cpuShares" />
<span class="input-help">{{ $t('container.cpuShareHelper') }}</span>
</el-form-item>
<el-form-item
@ -200,7 +233,7 @@
prop="nanoCPUs"
:rules="checkFloatNumberRange(0, Number(limits.cpu))"
>
<el-input class="mini-form-item" v-model="dialogData.rowData!.nanoCPUs">
<el-input class="mini-form-item" v-model="form.nanoCPUs">
<template #append>
<div style="width: 35px">{{ $t('commons.units.core') }}</div>
</template>
@ -214,7 +247,7 @@
prop="memory"
:rules="checkFloatNumberRange(0, Number(limits.memory))"
>
<el-input class="mini-form-item" v-model="dialogData.rowData!.memory">
<el-input class="mini-form-item" v-model="form.memory">
<template #append><div style="width: 35px">MB</div></template>
</el-input>
<span class="input-help">{{ $t('container.limitHelper', [limits.memory]) }}MB</span>
@ -224,7 +257,7 @@
type="textarea"
:placeholder="$t('container.tagHelper')"
:rows="3"
v-model="dialogData.rowData!.labelsStr"
v-model="form.labelsStr"
/>
</el-form-item>
<el-form-item :label="$t('container.env')" prop="envStr">
@ -232,21 +265,22 @@
type="textarea"
:placeholder="$t('container.tagHelper')"
:rows="3"
v-model="dialogData.rowData!.envStr"
v-model="form.envStr"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button :disabled="loading" @click="drawerVisible = false">
{{ $t('commons.button.cancel') }}
<el-button :disabled="loading" @click="goBack">
{{ $t('commons.button.back') }}
</el-button>
<el-button :disabled="loading" type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</el-col>
</el-row>
</el-form>
</template>
</DrawerPro>
</LayoutContent>
</div>
</template>
<script lang="ts" setup>
@ -262,6 +296,7 @@ import {
loadResourceLimit,
listNetwork,
searchContainer,
loadContainerInfo,
} from '@/api/modules/container';
import { Container } from '@/api/interface/container';
import { MsgError, MsgSuccess } from '@/utils/message';
@ -269,61 +304,90 @@ import { checkIpV4V6, checkPort } from '@/utils/util';
import router from '@/routers';
const loading = ref(false);
interface DialogProps {
title: string;
rowData?: Container.ContainerHelper;
getTableList?: () => Promise<any>;
}
const title = ref<string>('');
const drawerVisible = ref(false);
const dialogData = ref<DialogProps>({
title: '',
const isCreate = ref();
const form = reactive<Container.ContainerHelper>({
containerID: '',
name: '',
image: '',
imageInput: false,
forcePull: false,
network: '',
ipv4: '',
ipv6: '',
cmdStr: '',
entrypointStr: '',
memoryItem: 0,
cmd: [],
openStdin: false,
tty: false,
entrypoint: [],
publishAllPorts: false,
exposedPorts: [],
nanoCPUs: 0,
cpuShares: 1024,
memory: 0,
volumes: [],
privileged: false,
autoRemove: false,
labels: [],
labelsStr: '',
env: [],
envStr: '',
restartPolicy: 'no',
});
const acceptParams = (params: DialogProps): void => {
dialogData.value = params;
title.value = i18n.global.t('container.' + dialogData.value.title);
if (params.title === 'edit') {
dialogData.value.rowData.memory = Number(dialogData.value.rowData.memory.toFixed(2));
dialogData.value.rowData.cmd = dialogData.value.rowData.cmd || [];
const search = async () => {
if (!isCreate.value) {
const res = await loadContainerInfo(form.containerID);
if (res.data) {
form.name = res.data.name;
form.image = res.data.image;
form.network = res.data.network;
form.ipv4 = res.data.ipv4;
form.ipv6 = res.data.ipv6;
form.openStdin = res.data.openStdin;
form.tty = res.data.tty;
form.publishAllPorts = res.data.publishAllPorts;
form.nanoCPUs = res.data.nanoCPUs;
form.cpuShares = res.data.cpuShares;
form.privileged = res.data.privileged;
form.autoRemove = res.data.autoRemove;
form.restartPolicy = res.data.restartPolicy;
form.memory = Number(res.data.memory.toFixed(2));
form.cmd = res.data.cmd || [];
let itemCmd = '';
for (const item of dialogData.value.rowData.cmd) {
for (const item of form.cmd) {
itemCmd += `'${item}' `;
}
dialogData.value.rowData.cmdStr = itemCmd ? itemCmd.substring(0, itemCmd.length - 1) : '';
form.cmdStr = itemCmd ? itemCmd.substring(0, itemCmd.length - 1) : '';
let itemEntrypoint = '';
if (dialogData.value.rowData?.entrypoint) {
for (const item of dialogData.value.rowData.entrypoint) {
if (res.data.entrypoint) {
for (const item of res.data.entrypoint) {
itemEntrypoint += `'${item}' `;
}
}
dialogData.value.rowData.entrypointStr = itemEntrypoint
? itemEntrypoint.substring(0, itemEntrypoint.length - 1)
: '';
dialogData.value.rowData.labels = dialogData.value.rowData.labels || [];
dialogData.value.rowData.env = dialogData.value.rowData.env || [];
dialogData.value.rowData.labelsStr = dialogData.value.rowData.labels.join('\n');
dialogData.value.rowData.envStr = dialogData.value.rowData.env.join('\n');
dialogData.value.rowData.exposedPorts = dialogData.value.rowData.exposedPorts || [];
for (const item of dialogData.value.rowData.exposedPorts) {
form.entrypointStr = itemEntrypoint ? itemEntrypoint.substring(0, itemEntrypoint.length - 1) : '';
form.labels = res.data.labels || [];
form.env = res.data.env || [];
form.labelsStr = res.data.labels.join('\n');
form.envStr = res.data.env.join('\n');
form.exposedPorts = res.data.exposedPorts || [];
for (const item of res.data.exposedPorts) {
if (item.hostIP) {
item.host = item.hostIP + ':' + item.hostPort;
} else {
item.host = item.hostPort;
}
}
dialogData.value.rowData.volumes = dialogData.value.rowData.volumes || [];
form.volumes = res.data.volumes || [];
}
}
loadLimit();
loadImageOptions();
loadVolumeOptions();
loadNetworkOptions();
drawerVisible.value = true;
};
const emit = defineEmits<{ (e: 'search'): void }>();
const images = ref();
const volumes = ref();
@ -333,11 +397,6 @@ const limits = ref<Container.ResourceLimit>({
memory: null as number,
});
const handleClose = () => {
emit('search');
drawerVisible.value = false;
};
const rules = reactive({
name: [Rules.requiredInput, Rules.containerName],
image: [Rules.imageName],
@ -349,6 +408,10 @@ const rules = reactive({
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const goBack = () => {
router.push({ name: 'Container' });
};
const handlePortsAdd = () => {
let item = {
host: '',
@ -357,10 +420,10 @@ const handlePortsAdd = () => {
hostPort: '',
protocol: 'tcp',
};
dialogData.value.rowData!.exposedPorts.push(item);
form.exposedPorts.push(item);
};
const handlePortsDelete = (index: number) => {
dialogData.value.rowData!.exposedPorts.splice(index, 1);
form.exposedPorts.splice(index, 1);
};
const goRouter = async () => {
@ -374,10 +437,10 @@ const handleVolumesAdd = () => {
containerDir: '',
mode: 'rw',
};
dialogData.value.rowData!.volumes.push(item);
form.volumes.push(item);
};
const handleVolumesDelete = (index: number) => {
dialogData.value.rowData!.volumes.splice(index, 1);
form.volumes.splice(index, 1);
};
const loadLimit = async () => {
@ -399,8 +462,8 @@ const loadNetworkOptions = async () => {
networks.value = res.data;
};
const onSubmit = async (formEl: FormInstance | undefined) => {
if (dialogData.value.rowData!.volumes.length !== 0) {
for (const item of dialogData.value.rowData!.volumes) {
if (form.volumes.length !== 0) {
for (const item of form.volumes) {
if (!item.containerDir || !item.sourceDir) {
MsgError(i18n.global.t('container.volumeHelper'));
return;
@ -410,62 +473,60 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
if (dialogData.value.rowData?.envStr) {
dialogData.value.rowData.env = dialogData.value.rowData!.envStr.split('\n');
if (form.envStr) {
form.env = form.envStr.split('\n');
}
if (dialogData.value.rowData?.labelsStr) {
dialogData.value.rowData!.labels = dialogData.value.rowData!.labelsStr.split('\n');
if (form.labelsStr) {
form.labels = form.labelsStr.split('\n');
}
dialogData.value.rowData!.cmd = [];
if (dialogData.value.rowData?.cmdStr) {
if (dialogData.value.rowData?.cmdStr.indexOf(`'`) !== -1) {
let itemCmd = dialogData.value.rowData!.cmdStr.split(`'`);
form.cmd = [];
if (form.cmdStr) {
if (form.cmdStr.indexOf(`'`) !== -1) {
let itemCmd = form.cmdStr.split(`'`);
for (const cmd of itemCmd) {
if (cmd && cmd !== ' ') {
dialogData.value.rowData!.cmd.push(cmd);
form.cmd.push(cmd);
}
}
} else {
let itemCmd = dialogData.value.rowData!.cmdStr.split(` `);
let itemCmd = form.cmdStr.split(` `);
for (const cmd of itemCmd) {
dialogData.value.rowData!.cmd.push(cmd);
form.cmd.push(cmd);
}
}
}
dialogData.value.rowData!.entrypoint = [];
if (dialogData.value.rowData?.entrypointStr) {
if (dialogData.value.rowData?.entrypointStr.indexOf(`'`) !== -1) {
let itemEntrypoint = dialogData.value.rowData!.entrypointStr.split(`'`);
form.entrypoint = [];
if (form.entrypointStr) {
if (form.entrypointStr.indexOf(`'`) !== -1) {
let itemEntrypoint = form.entrypointStr.split(`'`);
for (const entry of itemEntrypoint) {
if (entry && entry !== ' ') {
dialogData.value.rowData!.entrypoint.push(entry);
form.entrypoint.push(entry);
}
}
} else {
let itemEntrypoint = dialogData.value.rowData!.entrypointStr.split(` `);
let itemEntrypoint = form.entrypointStr.split(` `);
for (const entry of itemEntrypoint) {
dialogData.value.rowData!.entrypoint.push(entry);
form.entrypoint.push(entry);
}
}
}
if (dialogData.value.rowData!.publishAllPorts) {
dialogData.value.rowData!.exposedPorts = [];
if (form.publishAllPorts) {
form.exposedPorts = [];
} else {
if (!checkPortValid()) {
return;
}
}
dialogData.value.rowData!.memory = Number(dialogData.value.rowData!.memory);
dialogData.value.rowData!.nanoCPUs = Number(dialogData.value.rowData!.nanoCPUs);
form.memory = Number(form.memory);
form.nanoCPUs = Number(form.nanoCPUs);
loading.value = true;
if (dialogData.value.title === 'create') {
await createContainer(dialogData.value.rowData!)
if (isCreate.value) {
await createContainer(form)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisible.value = false;
})
.catch(() => {
loading.value = false;
@ -480,12 +541,10 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
},
)
.then(async () => {
await updateContainer(dialogData.value.rowData!)
await updateContainer(form)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisible.value = false;
})
.catch(() => {
updateContainerID();
@ -504,24 +563,24 @@ const updateContainerID = async () => {
page: 1,
pageSize: 1,
state: 'all',
name: dialogData.value.rowData.name,
name: form.name,
filters: '',
orderBy: 'created_at',
order: 'null',
};
await searchContainer(params).then((res) => {
if (res.data.items?.length === 1) {
dialogData.value.rowData.containerID = res.data.items[0].containerID;
form.containerID = res.data.items[0].containerID;
return;
}
});
};
const checkPortValid = () => {
if (dialogData.value.rowData!.exposedPorts.length === 0) {
if (form.exposedPorts.length === 0) {
return true;
}
for (const port of dialogData.value.rowData!.exposedPorts) {
for (const port of form.exposedPorts) {
if (port.host.indexOf(':') !== -1) {
port.hostIP = port.host.substring(0, port.host.lastIndexOf(':'));
if (checkIpV4V6(port.hostIP)) {
@ -573,8 +632,14 @@ const isFromApp = (rowData: Container.ContainerHelper) => {
}
return false;
};
defineExpose({
acceptParams,
onMounted(() => {
if (router.currentRoute.value.query.containerID) {
isCreate.value = false;
form.containerID = String(router.currentRoute.value.query.containerID);
} else {
isCreate.value = true;
}
search();
});
</script>