feat: Add container orchestration navigation in app store (#9950)

Refs #8431
This commit is contained in:
ssongliu 2025-08-11 18:28:44 +08:00 committed by GitHub
parent 4df0c3e461
commit 6a8bd11fc9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 206 additions and 193 deletions

View file

@ -229,15 +229,16 @@ type BatchDelete struct {
}
type ComposeInfo struct {
Name string `json:"name"`
CreatedAt string `json:"createdAt"`
CreatedBy string `json:"createdBy"`
ContainerNumber int `json:"containerNumber"`
ConfigFile string `json:"configFile"`
Workdir string `json:"workdir"`
Path string `json:"path"`
Containers []ComposeContainer `json:"containers"`
Env []string `json:"env"`
Name string `json:"name"`
CreatedAt string `json:"createdAt"`
CreatedBy string `json:"createdBy"`
ContainerCount int `json:"containerCount"`
RunningCount int `json:"runningCount"`
ConfigFile string `json:"configFile"`
Workdir string `json:"workdir"`
Path string `json:"path"`
Containers []ComposeContainer `json:"containers"`
Env []string `json:"env"`
}
type ComposeContainer struct {
ContainerID string `json:"containerID"`

View file

@ -10,12 +10,9 @@ import (
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/docker/docker/api/types/container"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
@ -24,6 +21,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/compose"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"golang.org/x/net/context"
)
@ -54,14 +52,13 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
}
composeCreatedByLocal, _ := composeRepo.ListRecord()
composeLocalMap := make(map[string]dto.ComposeInfo)
for _, localItem := range composeCreatedByLocal {
composeItemLocal := dto.ComposeInfo{
ContainerNumber: 0,
CreatedAt: localItem.CreatedAt.Format(constant.DateTimeLayout),
ConfigFile: localItem.Path,
Workdir: strings.TrimSuffix(localItem.Path, "/docker-compose.yml"),
ContainerCount: 0,
CreatedAt: localItem.CreatedAt.Format(constant.DateTimeLayout),
ConfigFile: localItem.Path,
Workdir: strings.TrimSuffix(localItem.Path, "/docker-compose.yml"),
}
composeItemLocal.CreatedBy = "1Panel"
composeItemLocal.Path = localItem.Path
@ -78,18 +75,24 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
CreateTime: time.Unix(container.Created, 0).Format(constant.DateTimeLayout),
}
if compose, has := composeMap[name]; has {
compose.ContainerNumber++
compose.ContainerCount++
if strings.ToLower(containerItem.State) == "running" {
compose.RunningCount++
}
compose.Containers = append(compose.Containers, containerItem)
composeMap[name] = compose
} else {
config := container.Labels[composeConfigLabel]
workdir := container.Labels[composeWorkdirLabel]
composeItem := dto.ComposeInfo{
ContainerNumber: 1,
CreatedAt: time.Unix(container.Created, 0).Format(constant.DateTimeLayout),
ConfigFile: config,
Workdir: workdir,
Containers: []dto.ComposeContainer{containerItem},
ContainerCount: 1,
CreatedAt: time.Unix(container.Created, 0).Format(constant.DateTimeLayout),
ConfigFile: config,
Workdir: workdir,
Containers: []dto.ComposeContainer{containerItem},
}
if strings.ToLower(containerItem.State) == "running" {
composeItem.RunningCount = 1
}
createdBy, ok := container.Labels[composeCreatedBy]
if ok {
@ -118,8 +121,8 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
}
for key, item := range composeMap {
if existingItem, exists := mergedMap[key]; exists {
if item.ContainerNumber > 0 {
if existingItem.ContainerNumber <= 0 {
if item.ContainerCount > 0 {
if existingItem.ContainerCount <= 0 {
mergedMap[key] = item
}
}

View file

@ -275,7 +275,8 @@ export namespace Container {
name: string;
createdAt: string;
createdBy: string;
containerNumber: number;
containerCount: number;
runningCount: number;
configFile: string;
workdir: string;
path: string;

View file

@ -930,7 +930,7 @@ const message = {
containerNumber: 'Container number',
containerStatus: 'Container status',
exited: 'Exited',
running: 'Running',
running: 'Running ( {0} / {1} )',
composeDetailHelper:
'The compose is created external to 1Panel. The start and stop operations are not supported.',
composeOperatorHelper: '{1} operation will be performed on {0}. Do you want to continue?',

View file

@ -904,7 +904,7 @@ const message = {
containerNumber: 'コンテナ番号',
containerStatus: 'コンテナステータス',
exited: '終了',
running: 'ランニング',
running: 'ランニング ( {0} / {1} )',
composeDetailHelper: '構成は1パネルの外部に作成されます開始および停止操作はサポートされていません',
composeOperatorHelper: '{1}操作は{0}で実行されます続けたいですか',
composeDownHelper:

View file

@ -895,7 +895,7 @@ const message = {
containerNumber: '컨테이너 ',
containerStatus: '컨테이너 상태',
exited: '종료됨',
running: '실행 ',
running: '실행 ( {0} / {1} )',
composeDetailHelper: ' 컴포즈는 1Panel 외부에서 생성되었습니다. 시작 중지 작업은 지원되지 않습니다.',
composeOperatorHelper: '{1} 작업이 {0}에서 수행됩니다. 계속 하시겠습니까?',
composeDownHelper:

View file

@ -923,7 +923,7 @@ const message = {
containerNumber: 'Bilangan kontena',
containerStatus: 'Status kontena',
exited: 'Keluar',
running: 'Berjalan',
running: 'Berjalan ( {0} / {1} )',
composeDetailHelper: 'Komposisi dibuat di luar 1Panel. Operasi mula dan berhenti tidak disokong.',
composeOperatorHelper: 'Operasi {1} akan dilakukan pada {0}. Adakah anda mahu meneruskan?',
composeDownHelper:

View file

@ -917,7 +917,7 @@ const message = {
containerNumber: 'Número de containers',
containerStatus: 'Status do container',
exited: 'Finalizado',
running: 'Em execução',
running: 'Em execução ( {0} / {1} )',
composeDetailHelper:
'A composição foi criada externamente ao 1Panel. As operações de iniciar e parar não são suportadas.',
composeOperatorHelper: 'A operação {1} será realizada no {0}. Deseja continuar?',

View file

@ -917,7 +917,7 @@ const message = {
containerNumber: 'Количество контейнеров',
containerStatus: 'Статус контейнера',
exited: 'Завершен',
running: 'Работает',
running: 'Работает ( {0} / {1} )',
composeDetailHelper: 'Compose создан вне 1Panel. Операции запуска и остановки не поддерживаются.',
composeOperatorHelper: 'Операция {1} будет выполнена для {0}. Хотите продолжить?',
composeDownHelper: 'Это остановит и удалит все контейнеры и сети под compose {0}. Хотите продолжить?',

View file

@ -943,7 +943,7 @@ const message = {
containerNumber: 'Konteyner sayısı',
containerStatus: 'Konteyner durumu',
exited: 'Çıktı',
running: 'Çalışıyor',
running: 'Çalışıyor ( {0} / {1} )',
composeDetailHelper: 'Compose, 1Panel dışında oluşturulmuştur. Başlatma ve durdurma işlemleri desteklenmez.',
composeOperatorHelper: '{0} üzerinde {1} işlemi gerçekleştirilecek. Devam etmek istiyor musunuz?',
composeDownHelper:

View file

@ -891,7 +891,7 @@ const message = {
containerNumber: '容器數量',
containerStatus: '容器狀態',
exited: '已停止',
running: '運行中',
running: '運行中 ( {0} / {1} )',
composeDetailHelper: ' compose 1Panel 編排外部創建暫不支持啟停操作',
composeOperatorHelper: '將對 {0} 進行 {1} 操作是否繼續',
composeDownHelper: '將停止並刪除 {0} 編排下所有容器及網絡是否繼續',

View file

@ -889,7 +889,7 @@ const message = {
containerNumber: '容器数量',
containerStatus: '容器状态',
exited: '已停止',
running: '运行中',
running: '运行中 ( {0} / {1} )',
composeDetailHelper: ' compose 1Panel 编排外部创建暂不支持启停操作',
composeOperatorHelper: '将对 {0} 进行 {1} 操作是否继续',
composeDownHelper: '将停止并删除 {0} 编排下所有容器及网络是否继续',

View file

@ -73,6 +73,8 @@ router.afterEach((to) => {
localStorage.setItem('cachedRoute' + to.meta.activeMenu, '/cronjobs/cronjob');
} else if (to.meta.activeMenu === '/containers' && to.path === '/containers/container/operate') {
localStorage.setItem('cachedRoute' + to.meta.activeMenu, '/containers/container');
} else if (to.meta.activeMenu === '/containers' && to.path === '/containers/compose/detail') {
localStorage.setItem('cachedRoute' + to.meta.activeMenu, '/containers/compose');
} else if (to.meta.activeMenu === '/toolbox' && to.path === '/toolbox/clam/setting') {
localStorage.setItem('cachedRoute' + to.meta.activeMenu, '/toolbox/clam');
} else {

View file

@ -57,7 +57,7 @@ const containerRouter = {
},
},
{
path: 'composeDetail/:filters?',
path: 'compose/detail',
name: 'ComposeDetail',
component: () => import('@/views/container/compose/detail/index.vue'),
props: true,

View file

@ -150,6 +150,29 @@
</el-button>
</el-tooltip>
</span>
<span class="ml-1">
<el-tooltip
v-if="mode !== 'upgrade'"
effect="dark"
:content="$t('container.compose')"
placement="top"
>
<el-button
type="primary"
link
@click="
routerToNameWithQuery('ComposeDetail', {
name: installed.name,
uncached: 'true',
})
"
>
<el-icon>
<SvgIcon iconName="p-docker" />
</el-icon>
</el-button>
</el-tooltip>
</span>
<span class="ml-1" v-if="mode === 'installed'">
<el-tooltip
effect="dark"
@ -433,7 +456,7 @@ import IgnoreApp from '@/views/app-store/installed/ignore/create/index.vue';
import { getAgentSettingByKey } from '@/api/modules/setting';
import Tags from '@/views/app-store/components/tag.vue';
import SvgIcon from '@/components/svg-icon/svg-icon.vue';
import { routerToFileWithPath } from '@/utils/router';
import { routerToFileWithPath, routerToNameWithQuery } from '@/utils/router';
const data = ref<any>();
const loading = ref(false);

View file

@ -1,63 +1,31 @@
<template>
<div v-loading="loading">
<div class="app-status card-interval">
<el-card>
<el-row :gutter="20" class="items-center">
<div class="ml-5">
<el-tag effect="dark" type="success">{{ composeName }}</el-tag>
</div>
<div v-if="createdBy === '1Panel'" class="ml-10">
<el-button link type="primary" @click="onComposeOperate('up')">
{{ $t('commons.operate.start') }}
</el-button>
<el-divider direction="vertical" />
<el-button link type="primary" @click="onComposeOperate('stop')">
{{ $t('commons.operate.stop') }}
</el-button>
<el-divider direction="vertical" />
<el-button link type="primary" @click="onComposeOperate('down')">
{{ $t('commons.button.delete') }}
</el-button>
</div>
<div v-else>
<el-alert
style="margin-top: -5px; margin-left: 50px"
:closable="false"
show-icon
:title="$t('container.composeDetailHelper')"
type="info"
/>
</div>
</el-row>
</el-card>
</div>
<LayoutContent
style="margin-top: 10px"
back-name="Compose"
:title="$t('container.containerList')"
:title="$t('container.containerList') + ' [ ' + composeName + ' ]'"
:reload="true"
>
<template #main>
<el-button-group>
<el-button :disabled="checkStatus('start')" @click="onOperate('start')">
<el-button :disabled="checkStatus('start', null)" @click="onOperate('start', null)">
{{ $t('commons.operate.start') }}
</el-button>
<el-button :disabled="checkStatus('stop')" @click="onOperate('stop')">
<el-button :disabled="checkStatus('stop', null)" @click="onOperate('stop', null)">
{{ $t('commons.operate.stop') }}
</el-button>
<el-button :disabled="checkStatus('restart')" @click="onOperate('restart')">
<el-button :disabled="checkStatus('restart', null)" @click="onOperate('restart', null)">
{{ $t('commons.button.restart') }}
</el-button>
<el-button :disabled="checkStatus('kill')" @click="onOperate('kill')">
<el-button :disabled="checkStatus('kill', null)" @click="onOperate('kill', null)">
{{ $t('container.kill') }}
</el-button>
<el-button :disabled="checkStatus('pause')" @click="onOperate('pause')">
<el-button :disabled="checkStatus('pause', null)" @click="onOperate('pause', null)">
{{ $t('container.pause') }}
</el-button>
<el-button :disabled="checkStatus('unpause')" @click="onOperate('unpause')">
<el-button :disabled="checkStatus('unpause', null)" @click="onOperate('unpause', null)">
{{ $t('container.unpause') }}
</el-button>
<el-button :disabled="checkStatus('remove')" @click="onOperate('remove')">
<el-button :disabled="checkStatus('remove', null)" @click="onOperate('remove', null)">
{{ $t('commons.button.delete') }}
</el-button>
</el-button-group>
@ -88,9 +56,51 @@
min-width="100"
prop="imageName"
/>
<el-table-column :label="$t('commons.table.status')" min-width="50" prop="state" fix>
<el-table-column :label="$t('commons.table.status')" min-width="100" prop="state" sortable>
<template #default="{ row }">
<Status :key="row.state" :status="row.state"></Status>
<el-dropdown placement="bottom">
<Status :key="row.state" :status="row.state" :operate="true"></Status>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
:disabled="checkStatus('start', row)"
@click="onOperate('start', row)"
>
{{ $t('commons.operate.start') }}
</el-dropdown-item>
<el-dropdown-item
:disabled="checkStatus('stop', row)"
@click="onOperate('stop', row)"
>
{{ $t('commons.operate.stop') }}
</el-dropdown-item>
<el-dropdown-item
:disabled="checkStatus('restart', row)"
@click="onOperate('restart', row)"
>
{{ $t('commons.button.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 :label="$t('container.upTime')" min-width="100" prop="runTime" fix />
@ -128,35 +138,15 @@ import TerminalDialog from '@/views/container/container/terminal/index.vue';
import CodemirrorDrawer from '@/components/codemirror-pro/drawer.vue';
import Status from '@/components/status/index.vue';
import { dateFormat } from '@/utils/util';
import { composeOperator, containerOperator, inspect, searchContainer } from '@/api/modules/container';
import { ElMessageBox } from 'element-plus';
import { containerOperator, inspect, searchContainer } from '@/api/modules/container';
import i18n from '@/lang';
import { Container } from '@/api/interface/container';
import { MsgSuccess } from '@/utils/message';
import router from '@/routers';
const composeName = ref();
const composePath = ref();
const filters = ref();
const createdBy = ref();
const dialogContainerLogRef = ref();
const opRef = ref();
interface DialogProps {
createdBy: string;
name: string;
path: string;
filters: string;
}
const acceptParams = (props: DialogProps): void => {
composePath.value = props.path;
composeName.value = props.name;
filters.value = props.filters;
createdBy.value = props.createdBy;
search();
};
const data = ref();
const selects = ref<any>([]);
const paginationConfig = reactive({
@ -169,13 +159,12 @@ const paginationConfig = reactive({
const loading = ref(false);
const search = async () => {
let filterItem = filters.value;
let params = {
name: '',
state: 'all',
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
filters: filterItem,
filters: 'com.docker.compose.project=' + composeName.value,
orderBy: 'createdAt',
order: 'null',
};
@ -204,34 +193,35 @@ const onInspect = async (id: string) => {
myDetail.value!.acceptParams(param);
};
const checkStatus = (operation: string) => {
if (selects.value.length < 1) {
const checkStatus = (operation: string, row: Container.ContainerInfo | null) => {
let opList = row ? [row] : selects.value;
if (opList.length < 1) {
return true;
}
switch (operation) {
case 'start':
for (const item of selects.value) {
for (const item of opList) {
if (item.state === 'running') {
return true;
}
}
return false;
case 'stop':
for (const item of selects.value) {
for (const item of opList) {
if (item.state === 'stopped' || item.state === 'exited') {
return true;
}
}
return false;
case 'pause':
for (const item of selects.value) {
for (const item of opList) {
if (item.state === 'paused' || item.state === 'exited') {
return true;
}
}
return false;
case 'unpause':
for (const item of selects.value) {
for (const item of opList) {
if (item.state !== 'paused') {
return true;
}
@ -240,54 +230,24 @@ const checkStatus = (operation: string) => {
}
};
const onOperate = async (op: string) => {
const onOperate = async (op: string, row: Container.ContainerInfo | null) => {
let opList = row ? [row] : selects.value;
let msg = i18n.global.t('container.operatorHelper', [i18n.global.t('container.' + op)]);
let names = [];
for (const item of selects.value) {
for (const item of opList) {
names.push(item.name);
if (item.isFromApp) {
msg = i18n.global.t('container.operatorAppHelper', [i18n.global.t('container.' + op)]);
}
}
const successMsg = `${i18n.global.t('container.' + op)}${i18n.global.t('commons.status.success')}`;
opRef.value.acceptParams({
title: i18n.global.t('container.' + op),
names: names,
msg: msg,
api: containerOperator,
params: { names: names, operation: op },
successMsg: `${i18n.global.t('container.' + op)}${i18n.global.t('commons.status.success')}`,
});
};
const onComposeOperate = async (operation: string) => {
let mes =
operation === 'down'
? i18n.global.t('container.composeDownHelper', [composeName.value])
: i18n.global.t('container.composeOperatorHelper', [
composeName.value,
i18n.global.t('container.' + operation),
]);
ElMessageBox.confirm(mes, i18n.global.t('container.' + operation), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
let params = {
name: composeName.value,
path: composePath.value,
operation: operation,
withFile: false,
};
loading.value = true;
await composeOperator(params)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
})
.catch(() => {
loading.value = false;
});
successMsg,
});
};
@ -329,8 +289,11 @@ const buttons = [
},
];
defineExpose({
acceptParams,
onMounted(() => {
if (router.currentRoute.value.query?.name) {
composeName.value = router.currentRoute.value.query.name;
}
search();
});
</script>
@ -338,8 +301,4 @@ defineExpose({
.app-content {
height: 50px;
}
body {
margin: 0;
}
</style>

View file

@ -1,8 +1,5 @@
<template>
<div v-loading="loading">
<div v-show="isOnDetail">
<ComposeDetail ref="composeDetailRef" />
</div>
<docker-status
v-model:isActive="isActive"
v-model:isExist="isExist"
@ -10,7 +7,7 @@
@search="search"
/>
<LayoutContent v-if="!isOnDetail && isExist" :title="$t('container.compose', 2)" :class="{ mask: !isActive }">
<LayoutContent v-if="isExist" :title="$t('container.compose', 2)" :class="{ mask: !isActive }">
<template #leftToolBar>
<el-button type="primary" @click="onOpenDialog()">
{{ $t('container.createCompose') }}
@ -60,21 +57,22 @@
</template>
</el-table-column>
<el-table-column :label="$t('container.containerStatus')" min-width="80" fix>
<template #default="scope">
<div>
{{ getContainerStatus(scope.row.containers) }}
</div>
<template #default="{ row }">
<el-text class="mx-1" v-if="row.containerCount == 0" type="danger">
{{ $t('container.exited') }}
</el-text>
<el-text
v-else
class="mx-1"
:type="row.containerCount === row.runningCount ? 'success' : 'warning'"
>
{{ $t('container.running', [row.runningCount, row.containerCount]) }}
</el-text>
</template>
</el-table-column>
<el-table-column
:label="$t('container.containerNumber')"
prop="containerNumber"
min-width="80"
fix
/>
<el-table-column :label="$t('commons.table.createdAt')" prop="createdAt" min-width="80" fix />
<fu-table-operations
width="200px"
width="300px"
:ellipsis="10"
:buttons="buttons"
:label="$t('commons.table.operate')"
@ -95,19 +93,17 @@ import { reactive, ref } from 'vue';
import EditDialog from '@/views/container/compose/edit/index.vue';
import CreateDialog from '@/views/container/compose/create/index.vue';
import DeleteDialog from '@/views/container/compose/delete/index.vue';
import ComposeDetail from '@/views/container/compose/detail/index.vue';
import { inspect, searchCompose } from '@/api/modules/container';
import { composeOperator, inspect, searchCompose } from '@/api/modules/container';
import DockerStatus from '@/views/container/docker-status/index.vue';
import i18n from '@/lang';
import { Container } from '@/api/interface/container';
import { routerToFileWithPath } from '@/utils/router';
import { routerToFileWithPath, routerToNameWithQuery } from '@/utils/router';
import { MsgInfo, MsgSuccess } from '@/utils/message';
const data = ref();
const selects = ref<any>([]);
const loading = ref(false);
const isOnDetail = ref(false);
const paginationConfig = reactive({
cacheSizeKey: 'container-compose-page-size',
currentPage: 1,
@ -144,28 +140,8 @@ const search = async () => {
});
};
const composeDetailRef = ref();
const loadDetail = async (row: Container.ComposeInfo) => {
let params = {
createdBy: row.createdBy,
name: row.name,
path: row.path,
filters: 'com.docker.compose.project=' + row.name,
};
isOnDetail.value = true;
composeDetailRef.value!.acceptParams(params);
};
const getContainerStatus = (containers) => {
const safeContainers = containers || [];
const runningCount = safeContainers.filter((container) => container.state.toLowerCase() === 'running').length;
const totalCount = safeContainers.length;
const statusText = runningCount > 0 ? 'Running' : 'Exited';
if (statusText === 'Exited') {
return i18n.global.t('container.exited');
} else {
return i18n.global.t('container.running') + ` (${runningCount}/${totalCount})`;
}
routerToNameWithQuery('ComposeDetail', { name: row.name });
};
const dialogRef = ref();
@ -195,6 +171,42 @@ const onEdit = async (row: Container.ComposeInfo) => {
dialogEditRef.value!.acceptParams(params);
};
const onComposeOperate = async (operation: string, row: any) => {
if (row.createdBy !== '1Panel' && row.createdBy !== 'App') {
MsgInfo(i18n.global.t('container.composeDetailHelper'));
return;
}
let mes =
operation === 'down'
? i18n.global.t('container.composeDownHelper', [row.name])
: i18n.global.t('container.composeOperatorHelper', [
row.name,
i18n.global.t('commons.operate.' + operation),
]);
ElMessageBox.confirm(mes, i18n.global.t('commons.operate.' + operation), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
let params = {
name: row.name,
path: row.path,
operation: operation,
withFile: false,
};
loading.value = true;
await composeOperator(params)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
})
.catch(() => {
loading.value = false;
});
});
};
const buttons = [
{
label: i18n.global.t('commons.button.edit'),
@ -206,7 +218,19 @@ const buttons = [
},
},
{
label: i18n.global.t('commons.button.delete'),
label: i18n.global.t('commons.operate.start'),
click: (row: Container.ComposeInfo) => {
onComposeOperate('up', row);
},
},
{
label: i18n.global.t('commons.operate.stop'),
click: (row: Container.ComposeInfo) => {
onComposeOperate('stop', row);
},
},
{
label: i18n.global.t('commons.operate.delete'),
click: (row: Container.ComposeInfo) => {
onDelete(row);
},