Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/lang/modules/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lang/modules/es-es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -961,13 +961,16 @@ 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',
input: 'Introducir manualmente',
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',
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lang/modules/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lang/modules/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lang/modules/ms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lang/modules/pt-br.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lang/modules/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lang/modules/tr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lang/modules/zh-Hant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lang/modules/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
140 changes: 128 additions & 12 deletions frontend/src/views/container/image/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<el-button type="primary" plain @click="onOpenPull">
{{ $t('container.imagePull') }}
</el-button>
<el-button type="primary" plain @click="onOpenload">
<el-button type="primary" plain @click="onOpenLoad">
{{ $t('container.importImage') }}
</el-button>
<el-button type="primary" plain @click="onOpenBuild">
Expand Down Expand Up @@ -102,7 +102,7 @@
/>
<fu-table-operations
width="250px"
:ellipsis="10"
:ellipsis="2"
:buttons="buttons"
:label="$t('commons.table.operate')"
/>
Expand All @@ -111,6 +111,36 @@
</LayoutContent>

<CodemirrorDrawer ref="myDetail" />
<DialogPro v-model="updateDialogVisible" :title="$t('commons.button.update')" @close="handleUpdateDialogClose">
<el-form label-position="top">
<el-form-item :label="$t('container.tag')">
<el-checkbox
class="w-full"
:model-value="updateCheckAll"
:indeterminate="updateIndeterminate"
@change="onUpdateCheckAllChange"
>
{{ $t('commons.table.all') }}
</el-checkbox>
<el-checkbox-group v-model="updateSelectedTags">
<el-checkbox v-for="tag in updateTagOptions" :key="tag" :label="tag">
{{ tag }}
</el-checkbox>
</el-checkbox-group>
<span class="input-help">{{ $t('container.imageUpdateHelper') }}</span>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleUpdateDialogClose">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button type="primary" @click="submitUpdateSelection">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</DialogPro>

<OpDialog ref="opRef" @search="search" />
<Pull ref="dialogPullRef" @search="search" />
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -184,6 +214,15 @@ const dialogSaveRef = ref();
const dialogBuildRef = ref();
const dialogDeleteRef = ref();
const dialogPruneRef = ref();
const updateDialogVisible = ref(false);
const updateTagOptions = ref<Array<string>>([]);
const updateSelectedTags = ref<Array<string>>([]);
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) {
Expand Down Expand Up @@ -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('<none>'));
};

const pullImageTags = async (tags: string[]) => {
const taskID = newUUID();
await imagePull({
taskID: taskID,
repoID: 0,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve registry repoID when pulling update tags

The update flow always sends repoID: 0, which bypasses configured image-repo credentials and forces an unauthenticated pull path. This breaks updates for tags hosted on private registries (or any registry that depends on saved repo auth), even though normal pull flow explicitly captures and submits repoID for that purpose (frontend/src/views/container/image/pull/index.vue). As a result, the new update action can fail for the exact images users most need to refresh.

Useful? React with 👍 / 👎.

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