mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-12-18 21:38:57 +08:00
feat: Support batch operations for image tags (#10597)
This commit is contained in:
parent
02c002bda6
commit
222181baff
14 changed files with 101 additions and 43 deletions
|
|
@ -36,8 +36,8 @@ type ImagePull struct {
|
|||
}
|
||||
|
||||
type ImageTag struct {
|
||||
SourceID string `json:"sourceID" validate:"required"`
|
||||
TargetName string `json:"targetName" validate:"required"`
|
||||
SourceID string `json:"sourceID" validate:"required"`
|
||||
Tags []string `json:"tags" validate:"required"`
|
||||
}
|
||||
|
||||
type ImagePush struct {
|
||||
|
|
|
|||
|
|
@ -363,9 +363,39 @@ func (u *ImageService) ImageTag(req dto.ImageTag) error {
|
|||
}
|
||||
defer client.Close()
|
||||
|
||||
if err := client.ImageTag(context.TODO(), req.SourceID, req.TargetName); err != nil {
|
||||
imageItem, err := client.ImageInspect(context.Background(), req.SourceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tag := range req.Tags {
|
||||
isNew := true
|
||||
for _, tagOld := range imageItem.RepoTags {
|
||||
if tag == tagOld {
|
||||
isNew = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if isNew {
|
||||
if err := client.ImageTag(context.TODO(), req.SourceID, tag); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, tagOld := range imageItem.RepoTags {
|
||||
isDel := true
|
||||
for _, tag := range req.Tags {
|
||||
if tag == tagOld {
|
||||
isDel = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if isDel {
|
||||
if _, err := client.ImageRemove(context.TODO(), tagOld, image.RemoveOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ export namespace Container {
|
|||
}
|
||||
export interface ImageTag {
|
||||
sourceID: string;
|
||||
targetName: string;
|
||||
tags: Array<string>;
|
||||
}
|
||||
export interface ImagePush {
|
||||
taskID: string;
|
||||
|
|
|
|||
|
|
@ -882,7 +882,6 @@ const message = {
|
|||
imagePushHelper:
|
||||
'Detected that this image has multiple tags. Please confirm that the image name used for pushing is: {0}',
|
||||
imageDelete: 'Image delete',
|
||||
imageTagDeleteHelper: 'Remove other tags associated with this image ID',
|
||||
repoName: 'Container registry',
|
||||
imageName: 'Image name',
|
||||
pull: 'Pull',
|
||||
|
|
@ -893,6 +892,7 @@ const message = {
|
|||
pathSelect: 'Path',
|
||||
label: 'Label',
|
||||
imageTag: 'Image tag',
|
||||
imageTagHelper: 'Supports setting multiple image tags, press Enter after entering each tag to continue',
|
||||
push: 'Push',
|
||||
fileName: 'Filename',
|
||||
export: 'Export',
|
||||
|
|
|
|||
|
|
@ -884,7 +884,6 @@ const message = {
|
|||
imagePushHelper:
|
||||
'Detected that this image has multiple tags. Please confirm that the image name used for pushing is: {0}',
|
||||
imageDelete: 'Eliminar imagen',
|
||||
imageTagDeleteHelper: 'Eliminar otras etiquetas asociadas con este ID de imagen',
|
||||
repoName: 'Repositorio de contenedores',
|
||||
imageName: 'Nombre de la imagen',
|
||||
pull: 'Descargar',
|
||||
|
|
@ -895,6 +894,8 @@ const message = {
|
|||
pathSelect: 'Ruta',
|
||||
label: 'Etiqueta',
|
||||
imageTag: 'Etiqueta de imagen',
|
||||
imageTagHelper:
|
||||
'Admite configurar múltiples etiquetas de imagen, presione Enter después de ingresar cada etiqueta para continuar',
|
||||
push: 'Subir',
|
||||
fileName: 'Nombre de archivo',
|
||||
export: 'Exportar',
|
||||
|
|
|
|||
|
|
@ -858,7 +858,6 @@ const message = {
|
|||
imagePushHelper:
|
||||
'このイメージに複数のタグが存在することが検出されました。プッシュ時に使用するイメージ名が以下であることを確認してください:{0}',
|
||||
imageDelete: '画像削除',
|
||||
imageTagDeleteHelper: 'この画像IDに関連付けられた他のタグを削除します',
|
||||
repoName: 'コンテナレジストリ',
|
||||
imageName: '画像名',
|
||||
pull: '引く',
|
||||
|
|
@ -869,6 +868,7 @@ const message = {
|
|||
pathSelect: 'パス',
|
||||
label: 'ラベル',
|
||||
imageTag: '画像タグ',
|
||||
imageTagHelper: '複数のイメージタグの設定をサポートし、各タグ入力後にEnterキーを押して続行します',
|
||||
push: '押す',
|
||||
fileName: 'ファイル名',
|
||||
export: '輸出',
|
||||
|
|
|
|||
|
|
@ -850,7 +850,6 @@ const message = {
|
|||
imagePushHelper:
|
||||
'이 이미지에 여러 태그가 있는 것으로 감지되었습니다. 푸시 시 사용할 이미지 이름이 다음인지 확인하세요: {0}',
|
||||
imageDelete: '이미지 삭제',
|
||||
imageTagDeleteHelper: '이 이미지 ID와 관련된 다른 태그를 제거합니다.',
|
||||
repoName: '컨테이너 저장소 이름',
|
||||
imageName: '이미지 이름',
|
||||
pull: '풀',
|
||||
|
|
@ -861,6 +860,7 @@ const message = {
|
|||
pathSelect: '경로',
|
||||
label: '레이블',
|
||||
imageTag: '이미지 태그',
|
||||
imageTagHelper: '여러 이미지 태그 설정을 지원하며, 각 태그 입력 후 Enter 키를 눌러 계속합니다',
|
||||
push: '푸시',
|
||||
fileName: '파일 이름',
|
||||
export: '내보내기',
|
||||
|
|
|
|||
|
|
@ -875,7 +875,6 @@ const message = {
|
|||
imagePushHelper:
|
||||
'Terdapat pengesahan bahawa imej ini mempunyai beberapa tag. Sila pastikan nama imej yang digunakan untuk menolak adalah: {0}',
|
||||
imageDelete: 'Padam imej',
|
||||
imageTagDeleteHelper: 'Buang tag lain yang berkaitan dengan ID imej ini',
|
||||
repoName: 'Pendaftaran kontena',
|
||||
imageName: 'Nama imej',
|
||||
pull: 'Tarik',
|
||||
|
|
@ -886,6 +885,8 @@ const message = {
|
|||
pathSelect: 'Laluan',
|
||||
label: 'Label',
|
||||
imageTag: 'Tag imej',
|
||||
imageTagHelper:
|
||||
'Menyokong penetapan berbilang tag imej, tekan Enter selepas memasukkan setiap tag untuk teruskan',
|
||||
push: 'Tekan',
|
||||
fileName: 'Nama fail',
|
||||
export: 'Eksport',
|
||||
|
|
|
|||
|
|
@ -870,7 +870,6 @@ const message = {
|
|||
imagePushHelper:
|
||||
'Detectado que esta imagem possui múltiplas tags. Por favor, confirme que o nome da imagem usada para push é: {0}',
|
||||
imageDelete: 'Excluir imagem',
|
||||
imageTagDeleteHelper: 'Remover outras tags associadas a este ID de imagem',
|
||||
repoName: 'Registro de contêiner',
|
||||
imageName: 'Nome da imagem',
|
||||
pull: 'Puxar',
|
||||
|
|
@ -881,6 +880,8 @@ const message = {
|
|||
pathSelect: 'Caminho',
|
||||
label: 'Etiqueta',
|
||||
imageTag: 'Tag de imagem',
|
||||
imageTagHelper:
|
||||
'Suporta definir múltiplas tags de imagem, pressione Enter após inserir cada tag para continuar',
|
||||
push: 'Enviar',
|
||||
fileName: 'Nome do arquivo',
|
||||
export: 'Exportar',
|
||||
|
|
|
|||
|
|
@ -873,7 +873,6 @@ const message = {
|
|||
imagePushHelper:
|
||||
'Обнаружено, что у этого образа несколько тегов. Подтвердите, что имя образа, используемое для отправки: {0}',
|
||||
imageDelete: 'Удалить образ',
|
||||
imageTagDeleteHelper: 'Удалить другие теги, связанные с этим ID образа',
|
||||
repoName: 'Реестр контейнеров',
|
||||
imageName: 'Имя образа',
|
||||
pull: 'Загрузить',
|
||||
|
|
@ -884,6 +883,8 @@ const message = {
|
|||
pathSelect: 'Путь',
|
||||
label: 'Метка',
|
||||
imageTag: 'Тег образа',
|
||||
imageTagHelper:
|
||||
'Поддерживает установку нескольких тегов образов, нажмите Enter после ввода каждого тега для продолжения',
|
||||
push: 'Отправить',
|
||||
fileName: 'Имя файла',
|
||||
export: 'Экспорт',
|
||||
|
|
|
|||
|
|
@ -891,7 +891,6 @@ const message = {
|
|||
imagePushHelper:
|
||||
'Bu imgenin birden fazla etiketi olduğu tespit edildi. Lütfen gönderimde kullanılan imge adının şu olduğunu onaylayın: {0}',
|
||||
imageDelete: 'İmaj sil',
|
||||
imageTagDeleteHelper: 'Bu imaj IDsi ile ilişkili diğer etiketleri kaldır',
|
||||
repoName: 'Konteyner kayıt defteri',
|
||||
imageName: 'İmaj adı',
|
||||
pull: 'Çek',
|
||||
|
|
@ -902,6 +901,8 @@ const message = {
|
|||
pathSelect: 'Yol',
|
||||
label: 'Etiket',
|
||||
imageTag: 'İmaj etiketi',
|
||||
imageTagHelper:
|
||||
"Birden fazla görüntü etiketi ayarlamayı destekler, her etiket girdikten sonra Enter'a basarak devam edin",
|
||||
push: 'Gönder',
|
||||
fileName: 'Dosya adı',
|
||||
export: 'Dışa aktar',
|
||||
|
|
|
|||
|
|
@ -845,7 +845,6 @@ const message = {
|
|||
imagePush: '推送鏡像',
|
||||
imagePushHelper: '檢測到該映像存在多個標籤,請確認推送時使用的映像名稱為:{0}',
|
||||
imageDelete: '刪除鏡像',
|
||||
imageTagDeleteHelper: '移除與該映像 ID 相關聯的其他標籤',
|
||||
repoName: '倉庫名',
|
||||
imageName: '鏡像名',
|
||||
httpRepo: 'http 倉庫新增授信需要重啟 docker 服務',
|
||||
|
|
@ -859,6 +858,7 @@ const message = {
|
|||
pathSelect: '路徑選擇',
|
||||
label: '標籤',
|
||||
imageTag: '鏡像標籤',
|
||||
imageTagHelper: '支援設定多個映像標籤,輸入一個標籤後回車繼續',
|
||||
push: '推送',
|
||||
fileName: '檔案名',
|
||||
export: '匯出',
|
||||
|
|
|
|||
|
|
@ -844,7 +844,6 @@ const message = {
|
|||
imagePush: '推送镜像',
|
||||
imagePushHelper: '检测到该镜像存在多个标签,请确认推送时使用的镜像名称为:{0}',
|
||||
imageDelete: '删除镜像',
|
||||
imageTagDeleteHelper: '移除与该镜像 ID 相关联的其他标签',
|
||||
repoName: '仓库名',
|
||||
imageName: '镜像名',
|
||||
httpRepo: 'http 仓库添加授信需要重启 docker 服务',
|
||||
|
|
@ -858,6 +857,7 @@ const message = {
|
|||
pathSelect: '路径选择',
|
||||
label: '标签',
|
||||
imageTag: '镜像标签',
|
||||
imageTagHelper: '支持设置多个镜像 tag,输入一个 tag 后回车继续',
|
||||
push: '推送',
|
||||
fileName: '文件名',
|
||||
export: '导出',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<DrawerPro v-model="drawerVisible" :header="$t('container.imageTag')" @close="handleClose" size="large">
|
||||
<el-form v-loading="loading" label-position="top" ref="formRef" :model="form" label-width="80px">
|
||||
<el-form v-loading="loading" label-position="top" ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item :label="$t('app.source')">
|
||||
<el-checkbox v-model="form.fromRepo">{{ $t('container.imageRepo') }}</el-checkbox>
|
||||
</el-form-item>
|
||||
|
|
@ -14,19 +14,15 @@
|
|||
<el-option v-for="item in repos" :key="item.id" :value="item.name" :label="item.name" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('container.imageTag')" :rules="Rules.imageName" prop="targetName">
|
||||
<el-input v-model="form.targetName" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="w-full">
|
||||
<el-checkbox v-model="form.deleteTag">
|
||||
{{ $t('container.imageTagDeleteHelper') }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<el-checkbox-group v-if="form.deleteTag" v-model="form.deleteTags">
|
||||
<el-checkbox v-for="item in tags" :key="item" :value="item" :label="item" />
|
||||
</el-checkbox-group>
|
||||
<el-form-item :label="$t('container.imageTag')" prop="tags">
|
||||
<el-input-tag ref="inputTagRef" @add-tag="handleAdd" v-model="form.tags">
|
||||
<template #tag="{ value }">
|
||||
<el-button @click="setInputValue(value)" size="small" link type="info">
|
||||
{{ value }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input-tag>
|
||||
<span class="input-help">{{ $t('container.imageTagHelper') }}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
|
|
@ -48,25 +44,44 @@ import { reactive, ref } from 'vue';
|
|||
import { Rules } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { ElForm } from 'element-plus';
|
||||
import { imageRemove, imageTag } from '@/api/modules/container';
|
||||
import { imageTag } from '@/api/modules/container';
|
||||
import { Container } from '@/api/interface/container';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
|
||||
const loading = ref(false);
|
||||
const inputTagRef = ref();
|
||||
|
||||
const drawerVisible = ref(false);
|
||||
const repos = ref();
|
||||
const tags = ref();
|
||||
const form = reactive({
|
||||
imageID: '',
|
||||
fromRepo: false,
|
||||
repo: '',
|
||||
originName: '',
|
||||
targetName: '',
|
||||
|
||||
deleteTag: false,
|
||||
deleteTags: [],
|
||||
tags: [],
|
||||
});
|
||||
const rules = reactive({
|
||||
tags: [{ validator: checkTags, trigger: 'blur', required: true }],
|
||||
});
|
||||
function checkTags(rule: any, value: any, callback: any) {
|
||||
if (value.length === 0) {
|
||||
return callback(new Error(i18n.global.t('commons.rule.requiredInput')));
|
||||
}
|
||||
for (const item of value) {
|
||||
if (item === '' || typeof item === 'undefined' || item == null) {
|
||||
return callback(new Error(i18n.global.t('commons.rule.imageName')));
|
||||
} else {
|
||||
const reg = /^[a-zA-Z0-9]{1}[a-z:@A-Z0-9_/.-]{0,255}$/;
|
||||
if (!reg.test(item) && item !== '') {
|
||||
return callback(new Error(i18n.global.t('commons.rule.imageName')));
|
||||
} else {
|
||||
return callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
interface DialogProps {
|
||||
repos: Array<Container.RepoOptions>;
|
||||
|
|
@ -78,13 +93,10 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
|
|||
drawerVisible.value = true;
|
||||
form.imageID = params.imageID;
|
||||
form.originName = params.tags?.length !== 0 ? params.tags[0] : '';
|
||||
form.targetName = params.tags?.length !== 0 ? params.tags[0] : '';
|
||||
form.tags = params.tags || [];
|
||||
form.fromRepo = false;
|
||||
form.repo = '';
|
||||
form.deleteTag = false;
|
||||
form.deleteTags = [];
|
||||
repos.value = params.repos;
|
||||
tags.value = params.tags;
|
||||
};
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
|
|
@ -95,21 +107,33 @@ const handleClose = () => {
|
|||
type FormInstance = InstanceType<typeof ElForm>;
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const handleAdd = (val: string) => {
|
||||
form.tags = form.tags?.filter((item) => item !== val);
|
||||
form.tags.push(val);
|
||||
};
|
||||
const setInputValue = async (text) => {
|
||||
await nextTick();
|
||||
const inputEl = inputTagRef.value?.$el?.querySelector('input');
|
||||
if (!inputEl) return;
|
||||
|
||||
inputEl.value = text;
|
||||
inputEl.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
inputEl.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
inputEl.setSelectionRange(text.length, text.length);
|
||||
};
|
||||
|
||||
const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
let params = {
|
||||
sourceID: form.imageID,
|
||||
targetName: form.targetName,
|
||||
tags: form.tags,
|
||||
};
|
||||
loading.value = true;
|
||||
await imageTag(params)
|
||||
.then(async () => {
|
||||
loading.value = false;
|
||||
if (form.deleteTag && form.deleteTags.length !== 0) {
|
||||
await imageRemove({ names: form.deleteTags });
|
||||
}
|
||||
drawerVisible.value = false;
|
||||
emit('search');
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
|
|
@ -122,12 +146,11 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
|||
|
||||
const changeRepo = (val) => {
|
||||
if (val === 'Docker Hub') {
|
||||
form.targetName = form.originName;
|
||||
return;
|
||||
}
|
||||
for (const item of repos.value) {
|
||||
if (item.name == val) {
|
||||
form.targetName = item.downloadUrl + '/' + form.originName;
|
||||
form.tags.push(item.downloadUrl + '/' + form.originName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue