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);
},
},
{