diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 846bab61ad44..ee7b091bc5e5 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -943,11 +943,13 @@ const message = { sameImageHelper: 'Containers using the same image can be batch upgraded after selection', targetImage: 'Target image', imageLoadErr: 'No image name detected for the container', + imageUpdateTagEmpty: 'No updatable image tags detected', appHelper: 'The container comes from the app store, and upgrading may make the service unavailable.', resource: 'Resource', input: 'Manually input', forcePull: 'Always pull image ', forcePullHelper: 'This will ignore existing images on the server and pull the latest image from the registry.', + imageUpdateHelper: 'Check the same tag in registry, and pull to update local image only if changed.', server: 'Host', serverExample: '80, 80-88, ip:80 or ip:80-88', containerExample: '80 or 80-88', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index fa25ae5bd056..79583735346a 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -961,6 +961,7 @@ const message = { 'Los contenedores que usan la misma imagen pueden actualizarse en lote después de seleccionarlos', targetImage: 'Imagen objetivo', imageLoadErr: 'No se detectó un nombre de imagen para el contenedor', + imageUpdateTagEmpty: 'No se detectaron etiquetas de imagen actualizables', appHelper: 'El contenedor proviene de la tienda de aplicaciones, y al actualizar podría hacer que el servicio no esté disponible.', resource: 'Recurso', @@ -968,6 +969,8 @@ const message = { forcePull: 'Siempre descargar imagen', forcePullHelper: 'Esto ignorará las imágenes existentes en el servidor y descargará la más reciente desde el repositorio.', + imageUpdateHelper: + 'Comprobar la misma etiqueta en el registro y, si hay cambios, descargar y actualizar la imagen local.', server: 'Servidor', serverExample: '80, 80-88, ip:80 o ip:80-88', containerExample: '80 o 80-88', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 10b97242074c..99e805cb3d9a 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -944,11 +944,13 @@ const message = { sameImageContainer: '同一イメージコンテナ', sameImageHelper: '同一イメージを使用するコンテナは選択後一括アップグレード可能', imageLoadErr: 'コンテナの画像名は検出されません', + imageUpdateTagEmpty: '更新可能なイメージタグが検出されません', appHelper: 'このコンテナはアプリストアから取得されたものであり、アップグレードによってサービスが利用不可になる可能性があります。', input: '手動入力', forcePull: '常に画像を引っ張ってください', forcePullHelper: 'これにより、サーバー上の既存の画像が無視され、レジストリから最新の画像が引き出されます。', + imageUpdateHelper: 'レジストリの同名タグを確認し、更新があれば取得してローカルイメージを更新します。', server: 'ホスト', serverExample: '80、80-88、IP:80またはIP:80-88', containerExample: '80または80-88', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 71dd3fbb139c..29e3ab50e6b7 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -931,10 +931,12 @@ const message = { sameImageHelper: '동일한 이미지를 사용하는 컨테이너는 선택 후 일괄 업그레이드 가능', targetImage: '대상 이미지', imageLoadErr: '컨테이너에 대한 이미지 이름이 감지되지 않았습니다.', + imageUpdateTagEmpty: '업데이트 가능한 이미지 태그를 찾지 못했습니다.', appHelper: '이 컨테이너는 앱 스토어에서 왔으며 업그레이드 시 서비스가 중단될 수 있습니다.', input: '수동 입력', forcePull: '이미지 강제 풀', forcePullHelper: '이 작업은 서버에 있는 기존 이미지를 무시하고 레지스트리에서 최신 이미지를 강제로 가져옵니다.', + imageUpdateHelper: '레지스트리의 동일 태그를 확인하고, 변경이 있으면 가져와 로컬 이미지를 업데이트합니다.', server: '호스트', serverExample: '80, 80-88, ip:80 또는 ip:80-88', containerExample: '80 또는 80-88', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 1c9728f75327..82d9f610f503 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -952,11 +952,14 @@ const message = { sameImageHelper: 'Kontena yang menggunakan imej sama boleh dinaik taraf secara berkumpulan setelah dipilih', targetImage: 'Imej sasaran', imageLoadErr: 'Tiada nama imej dikesan untuk kontena', + imageUpdateTagEmpty: 'Tiada tag imej yang boleh dikemas kini dikesan', appHelper: 'Kontena berasal dari gedung aplikasi, dan peningkatan boleh menyebabkan perkhidmatan tidak tersedia.', input: 'Input manual', forcePull: 'Tarik imej sentiasa ', forcePullHelper: 'Ini akan mengabaikan imej sedia ada di pelayan dan menarik imej terkini dari pendaftaran.', + imageUpdateHelper: + 'Semak tag yang sama di registry, jika ada kemas kini barulah tarik dan kemas kini imej tempatan.', server: 'Hos', serverExample: '80, 80-88, ip:80 atau ip:80-88', containerExample: '80 atau 80-88', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index f921f7de4119..cc2dce8b4c53 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -950,11 +950,14 @@ const message = { sameImageHelper: 'Contêineres usando a mesma imagem podem ser atualizados em lote após seleção', targetImage: 'Imagem alvo', imageLoadErr: 'Nenhum nome de imagem detectado para o contêiner', + imageUpdateTagEmpty: 'Nenhuma tag de imagem atualizável foi detectada', appHelper: 'O contêiner vem da loja de aplicativos, e o upgrade pode tornar o serviço indisponível.', input: 'Entrada manual', forcePull: 'Sempre puxar imagem', forcePullHelper: 'Isso ignorará as imagens existentes no servidor e puxará a imagem mais recente do repositório.', + imageUpdateHelper: + 'Verificar a mesma tag no registry e, se houver atualização, fazer pull e atualizar a imagem local.', server: 'Host', serverExample: '80, 80-88, ip:80 ou ip:80-88', containerExample: '80 ou 80-88', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 8d183c08c2e3..e682ed25b985 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -945,11 +945,14 @@ const message = { sameImageHelper: 'Контейнеры, использующие один образ, можно массово обновить после выбора', targetImage: 'Целевой образ', imageLoadErr: 'Не обнаружено имя образа для контейнера', + imageUpdateTagEmpty: 'Не обнаружены теги образов, доступные для обновления', appHelper: 'Контейнер происходит из магазина приложений, и обновление может сделать сервис недоступным.', input: 'Ручной ввод', forcePull: 'Всегда загружать образ', forcePullHelper: 'Это будет игнорировать существующие образы на сервере и загружать последний образ из реестра.', + imageUpdateHelper: + 'Проверить одноимённый тег в реестре и, если есть обновление, загрузить и обновить локальный образ.', server: 'Хост', serverExample: '80, 80-88, ip:80 или ip:80-88', containerExample: '80 или 80-88', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index eaf4342a5a49..be9562965175 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -951,11 +951,13 @@ const message = { sameImageHelper: 'Aynı imajı kullanan konteynerlar seçilerek toplu şekilde güncellenebilir', targetImage: 'Hedef imaj', imageLoadErr: 'Konteyner için imaj adı algılanmadı', + imageUpdateTagEmpty: 'Güncellenebilir imaj etiketi algılanmadı', appHelper: 'Konteyner uygulama mağazasından geliyor ve yükseltme hizmeti kullanılamaz hale getirebilir.', resource: 'Kaynak', input: 'Manuel giriş', forcePull: 'Her zaman imajı çek ', forcePullHelper: 'Bu, sunucudaki mevcut imajları yok sayacak ve kayıt defterinden en son imajı çekecektir.', + imageUpdateHelper: 'Kayıt deposundaki aynı etiketi kontrol et, güncelleme varsa çekip yerel imajı güncelle.', server: 'Ana bilgisayar', serverExample: '80, 80-88, ip:80 veya ip:80-88', containerExample: '80 veya 80-88', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index e503879a94f1..38e3417a49b2 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -896,11 +896,13 @@ const message = { sameImageHelper: '同映像容器可勾選後批次升級', targetImage: '目標映像', imageLoadErr: '未偵測到容器的映像名稱', + imageUpdateTagEmpty: '未偵測到可更新的映像標籤', appHelper: '該容器來源於應用商店,升級可能導致該服務不可用', resource: '資源', input: '手動輸入', forcePull: '強制拉取映像', forcePullHelper: '忽略伺服器已存在的映像,重新拉取一次', + imageUpdateHelper: '將檢查映像倉庫中的同名標籤,若有更新則拉取並更新本地映像。', server: '伺服器', serverExample: '80, 80-88, ip:80 或 ip:80-88', containerExample: '80 或 80-88', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 9108a288cd77..90692ab107d2 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -898,12 +898,14 @@ const message = { sameImageHelper: '同镜像容器可勾选后批量升级', targetImage: '目标镜像', imageLoadErr: '未检测到容器的镜像名称', + imageUpdateTagEmpty: '未检测到可更新的镜像标签', appHelper: '该容器来源于应用商店,升级可能导致该服务不可用', resource: '资源', input: '手动输入', forcePull: '强制拉取镜像', forcePullHelper: '忽略服务器已存在的镜像,重新拉取一次', + imageUpdateHelper: '将检查镜像仓库中的同名标签,若有更新则拉取并更新本地镜像。', server: '服务器', serverExample: '80, 80-88, ip:80 或 ip:80-88', containerExample: '80 或 80-88', diff --git a/frontend/src/views/container/image/index.vue b/frontend/src/views/container/image/index.vue index 5021414d1ca7..9dc37766dc72 100644 --- a/frontend/src/views/container/image/index.vue +++ b/frontend/src/views/container/image/index.vue @@ -13,7 +13,7 @@ {{ $t('container.imagePull') }} - + {{ $t('container.importImage') }} @@ -102,7 +102,7 @@ /> @@ -111,6 +111,36 @@ + + + + + {{ $t('commons.table.all') }} + + + + {{ tag }} + + + {{ $t('container.imageUpdateHelper') }} + + + + @@ -140,12 +170,12 @@ import Prune from '@/views/container/image/prune/index.vue'; import DockerStatus from '@/views/container/docker-status/index.vue'; import CodemirrorDrawer from '@/components/codemirror-pro/drawer.vue'; import TaskLog from '@/components/log/task/index.vue'; -import { searchImage, listImageRepo, imageRemove, inspect, containerPrune } from '@/api/modules/container'; +import { searchImage, listImageRepo, imageRemove, inspect, containerPrune, imagePull } from '@/api/modules/container'; import i18n from '@/lang'; import { GlobalStore } from '@/store'; import { ElMessageBox } from 'element-plus'; import { updateCommonDescription } from '@/api/modules/setting'; -import { MsgSuccess } from '@/utils/message'; +import { MsgError, MsgSuccess } from '@/utils/message'; const globalStore = GlobalStore(); const taskLogRef = ref(); @@ -184,6 +214,15 @@ const dialogSaveRef = ref(); const dialogBuildRef = ref(); const dialogDeleteRef = ref(); const dialogPruneRef = ref(); +const updateDialogVisible = ref(false); +const updateTagOptions = ref>([]); +const updateSelectedTags = ref>([]); +const updateCheckAll = computed( + () => updateTagOptions.value.length > 0 && updateSelectedTags.value.length === updateTagOptions.value.length, +); +const updateIndeterminate = computed( + () => updateSelectedTags.value.length > 0 && updateSelectedTags.value.length < updateTagOptions.value.length, +); const search = async (column?: any) => { if (!isActive.value || !isExist.value) { @@ -304,40 +343,117 @@ const openTaskLog = (taskID: string) => { taskLogRef.value.openWithTaskID(taskID); }; -const onOpenload = () => { +const onOpenLoad = () => { dialogLoadRef.value!.acceptParams(); }; +const normalizeImageTags = (tags: string[]) => { + return (tags || []).filter((tag) => tag && !tag.includes('')); +}; + +const pullImageTags = async (tags: string[]) => { + const taskID = newUUID(); + await imagePull({ + taskID: taskID, + repoID: 0, + imageName: tags, + }); + openTaskLog(taskID); +}; + +const runUpdate = async (tags: string[]) => { + const validTags = normalizeImageTags(tags); + if (validTags.length === 0) { + MsgError(i18n.global.t('container.imageUpdateTagEmpty')); + return; + } + try { + await ElMessageBox.confirm( + i18n.global.t('container.imageUpdateHelper'), + i18n.global.t('commons.button.update'), + { + confirmButtonText: i18n.global.t('commons.button.confirm'), + cancelButtonText: i18n.global.t('commons.button.cancel'), + type: 'info', + }, + ); + } catch { + return; + } + await pullImageTags(validTags); +}; + +const onUpdate = async (row: Container.ImageInfo) => { + const tags = normalizeImageTags(row.tags || []); + if (tags.length === 0) { + MsgError(i18n.global.t('container.imageUpdateTagEmpty')); + return; + } + if (tags.length === 1) { + await runUpdate(tags); + return; + } + updateTagOptions.value = tags; + updateSelectedTags.value = [...tags]; + updateDialogVisible.value = true; +}; + +const onUpdateCheckAllChange = (checked: boolean) => { + updateSelectedTags.value = checked ? [...updateTagOptions.value] : []; +}; + +const handleUpdateDialogClose = () => { + updateDialogVisible.value = false; + updateTagOptions.value = []; + updateSelectedTags.value = []; +}; + +const submitUpdateSelection = async () => { + if (updateSelectedTags.value.length === 0) { + MsgError(i18n.global.t('commons.msg.confirmNoNull', [i18n.global.t('container.tag')])); + return; + } + const selected = [...updateSelectedTags.value]; + handleUpdateDialogClose(); + await runUpdate(selected); +}; + const buttons = [ { - label: i18n.global.t('container.tag'), + label: i18n.global.t('container.push'), click: (row: Container.ImageInfo) => { let params = { repos: repos.value, - imageID: row.id, tags: row.tags, }; - dialogTagRef.value!.acceptParams(params); + dialogPushRef.value!.acceptParams(params); }, }, { - label: i18n.global.t('container.push'), + label: i18n.global.t('container.export'), click: (row: Container.ImageInfo) => { let params = { repos: repos.value, tags: row.tags, }; - dialogPushRef.value!.acceptParams(params); + dialogSaveRef.value!.acceptParams(params); }, }, { - label: i18n.global.t('container.export'), + label: i18n.global.t('commons.button.update'), + click: (row: Container.ImageInfo) => { + onUpdate(row); + }, + }, + { + label: i18n.global.t('container.tag'), click: (row: Container.ImageInfo) => { let params = { repos: repos.value, + imageID: row.id, tags: row.tags, }; - dialogSaveRef.value!.acceptParams(params); + dialogTagRef.value!.acceptParams(params); }, }, {