diff --git a/agent/app/dto/container.go b/agent/app/dto/container.go index 0d119db27..e6d9f8188 100644 --- a/agent/app/dto/container.go +++ b/agent/app/dto/container.go @@ -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"` diff --git a/agent/app/service/container_compose.go b/agent/app/service/container_compose.go index 51b01fb15..04457b8b0 100644 --- a/agent/app/service/container_compose.go +++ b/agent/app/service/container_compose.go @@ -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 } } diff --git a/frontend/src/api/interface/container.ts b/frontend/src/api/interface/container.ts index 1260d5caf..7726f399f 100644 --- a/frontend/src/api/interface/container.ts +++ b/frontend/src/api/interface/container.ts @@ -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; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index da61ee22c..5295bef48 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -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?', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index a834102a2..1c72d661d 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -904,7 +904,7 @@ const message = { containerNumber: 'コンテナ番号', containerStatus: 'コンテナステータス', exited: '終了', - running: 'ランニング', + running: 'ランニング ( {0} / {1} )', composeDetailHelper: '構成は1パネルの外部に作成されます。開始および停止操作はサポートされていません。', composeOperatorHelper: '{1}操作は{0}で実行されます。続けたいですか?', composeDownHelper: diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 8eaf1de09..465b0ddcd 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -895,7 +895,7 @@ const message = { containerNumber: '컨테이너 수', containerStatus: '컨테이너 상태', exited: '종료됨', - running: '실행 중', + running: '실행 중 ( {0} / {1} )', composeDetailHelper: '이 컴포즈는 1Panel 외부에서 생성되었습니다. 시작 및 중지 작업은 지원되지 않습니다.', composeOperatorHelper: '{1} 작업이 {0}에서 수행됩니다. 계속 하시겠습니까?', composeDownHelper: diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index e0123cf99..b3a32f5ea 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -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: diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 7db56436f..1d1d5bd20 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -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?', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index da412e3de..555794e68 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -917,7 +917,7 @@ const message = { containerNumber: 'Количество контейнеров', containerStatus: 'Статус контейнера', exited: 'Завершен', - running: 'Работает', + running: 'Работает ( {0} / {1} )', composeDetailHelper: 'Compose создан вне 1Panel. Операции запуска и остановки не поддерживаются.', composeOperatorHelper: 'Операция {1} будет выполнена для {0}. Хотите продолжить?', composeDownHelper: 'Это остановит и удалит все контейнеры и сети под compose {0}. Хотите продолжить?', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index acd23a49e..708f1b9dd 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -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: diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 11e57d8de..959c24f1f 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -891,7 +891,7 @@ const message = { containerNumber: '容器數量', containerStatus: '容器狀態', exited: '已停止', - running: '運行中', + running: '運行中 ( {0} / {1} )', composeDetailHelper: '該 compose 為 1Panel 編排外部創建。暫不支持啟停操作。', composeOperatorHelper: '將對 {0} 進行 {1} 操作,是否繼續?', composeDownHelper: '將停止並刪除 {0} 編排下所有容器及網絡,是否繼續?', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index e06f0e71b..488ea3602 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -889,7 +889,7 @@ const message = { containerNumber: '容器数量', containerStatus: '容器状态', exited: '已停止', - running: '运行中', + running: '运行中 ( {0} / {1} )', composeDetailHelper: '该 compose 为 1Panel 编排外部创建。暂不支持启停操作。', composeOperatorHelper: '将对 {0} 进行 {1} 操作,是否继续?', composeDownHelper: '将停止并删除 {0} 编排下所有容器及网络,是否继续?', diff --git a/frontend/src/routers/index.ts b/frontend/src/routers/index.ts index 365551266..6fe149b76 100644 --- a/frontend/src/routers/index.ts +++ b/frontend/src/routers/index.ts @@ -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 { diff --git a/frontend/src/routers/modules/container.ts b/frontend/src/routers/modules/container.ts index b4f290f51..c66b6af6e 100644 --- a/frontend/src/routers/modules/container.ts +++ b/frontend/src/routers/modules/container.ts @@ -57,7 +57,7 @@ const containerRouter = { }, }, { - path: 'composeDetail/:filters?', + path: 'compose/detail', name: 'ComposeDetail', component: () => import('@/views/container/compose/detail/index.vue'), props: true, diff --git a/frontend/src/views/app-store/installed/index.vue b/frontend/src/views/app-store/installed/index.vue index 47c804d74..4d21a1aaf 100644 --- a/frontend/src/views/app-store/installed/index.vue +++ b/frontend/src/views/app-store/installed/index.vue @@ -150,6 +150,29 @@ + + + + + + + + + (); const loading = ref(false); diff --git a/frontend/src/views/container/compose/detail/index.vue b/frontend/src/views/container/compose/detail/index.vue index 5329545d1..5639e9664 100644 --- a/frontend/src/views/container/compose/detail/index.vue +++ b/frontend/src/views/container/compose/detail/index.vue @@ -1,63 +1,31 @@ - - - - - {{ composeName }} - - - - {{ $t('commons.operate.start') }} - - - - {{ $t('commons.operate.stop') }} - - - - {{ $t('commons.button.delete') }} - - - - - - - - - + {{ $t('commons.operate.start') }} - + {{ $t('commons.operate.stop') }} - + {{ $t('commons.button.restart') }} - + {{ $t('container.kill') }} - + {{ $t('container.pause') }} - + {{ $t('container.unpause') }} - + {{ $t('commons.button.delete') }} @@ -88,9 +56,51 @@ min-width="100" prop="imageName" /> - + - + + + + + + {{ $t('commons.operate.start') }} + + + {{ $t('commons.operate.stop') }} + + + {{ $t('commons.button.restart') }} + + + {{ $t('container.kill') }} + + + {{ $t('container.pause') }} + + + {{ $t('container.unpause') }} + + + + @@ -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([]); 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(); }); @@ -338,8 +301,4 @@ defineExpose({ .app-content { height: 50px; } - -body { - margin: 0; -} diff --git a/frontend/src/views/container/compose/index.vue b/frontend/src/views/container/compose/index.vue index a86184915..2eb71a6e1 100644 --- a/frontend/src/views/container/compose/index.vue +++ b/frontend/src/views/container/compose/index.vue @@ -1,8 +1,5 @@ - - - - + {{ $t('container.createCompose') }} @@ -60,21 +57,22 @@ - - - {{ getContainerStatus(scope.row.containers) }} - + + + {{ $t('container.exited') }} + + + {{ $t('container.running', [row.runningCount, row.containerCount]) }} + - ([]); 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); },