mirror of
https://github.com/rustfs/console.git
synced 2026-10-31 03:52:11 +08:00
feat : delete all version selection
This commit is contained in:
+117
-11
@@ -85,11 +85,48 @@
|
||||
"
|
||||
/>
|
||||
<object-info ref="infoRef" :bucket-name="bucketName" @refresh-parent="handleObjectDeleted" />
|
||||
|
||||
<AlertDialog v-model:open="deleteDialogOpen">
|
||||
<AlertDialogContent class="sm:max-w-md">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ t('Warning') }}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<p>{{ t('Are you sure you want to delete the selected objects?') }}</p>
|
||||
<div v-if="bucketVersioningEnabled" class="mt-4 flex items-center gap-2">
|
||||
<Checkbox v-model="deleteAllVersions" />
|
||||
<span>{{ t('Delete All Versions') }}</span>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel as-child>
|
||||
<Button variant="outline" class="w-full sm:w-auto text-foreground">
|
||||
{{ t('Cancel') }}
|
||||
</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction as-child @click.prevent="handleConfirmDelete">
|
||||
<Button variant="destructive" class="w-full sm:w-auto text-white">
|
||||
{{ t('Confirm') }}
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
|
||||
@@ -110,7 +147,6 @@ import { useTaskManagerStore } from '~/store/tasks'
|
||||
const { $s3Client } = useNuxtApp()
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const dialog = useDialog()
|
||||
const props = defineProps<{ bucket: string; path: string }>()
|
||||
|
||||
const uploadPickerVisible = ref(false)
|
||||
@@ -120,6 +156,10 @@ const searchTerm = ref('')
|
||||
const loading = ref(false)
|
||||
const showDeleted = useLocalStorage('object-list-show-deleted', false)
|
||||
|
||||
const deleteDialogOpen = ref(false)
|
||||
const deleteDialogKeys = ref<string[]>([])
|
||||
const deleteAllVersions = ref(false)
|
||||
|
||||
const debounce = (fn: Function, delay: number) => {
|
||||
let timer: NodeJS.Timeout | null = null
|
||||
return (...args: any[]) => {
|
||||
@@ -170,6 +210,8 @@ const nextToken = ref<string | undefined>(undefined)
|
||||
const infoRef = ref<{ openDrawer: (bucket: string, key: string) => void }>()
|
||||
const message = useMessage()
|
||||
const objectApi = useObject({ bucket: bucketName.value })
|
||||
const { getBucketVersioning } = useBucket({})
|
||||
const bucketVersioningEnabled = ref(false)
|
||||
|
||||
const handleNewObject = (asPrefix: boolean) => {
|
||||
newObjectAsPrefix.value = asPrefix
|
||||
@@ -180,6 +222,15 @@ const handleObjectDeleted = () => {
|
||||
refresh()
|
||||
}
|
||||
|
||||
const loadBucketVersioningStatus = async () => {
|
||||
try {
|
||||
const resp = await getBucketVersioning(bucketName.value)
|
||||
bucketVersioningEnabled.value = resp.Status === 'Enabled'
|
||||
} catch (error) {
|
||||
bucketVersioningEnabled.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchObjects = async (): Promise<ObjectRow[]> => {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -341,7 +392,7 @@ const columns = computed<ColumnDef<ObjectRow, any>[]>(() => {
|
||||
{
|
||||
variant: 'outline',
|
||||
size: 'sm',
|
||||
onClick: () => confirmDelete([row.original.Key]),
|
||||
onClick: () => openDeleteDialog([row.original.Key]),
|
||||
},
|
||||
() => [h(Icon, { name: 'ri:delete-bin-5-line', class: 'size-4' }), h('span', t('Delete'))]
|
||||
),
|
||||
@@ -371,6 +422,7 @@ const handleAllTasksCompleted = () => {
|
||||
onMounted(() => {
|
||||
// 监听所有任务全部完成事件(上传/删除)
|
||||
taskStore.on('drained', handleAllTasksCompleted)
|
||||
loadBucketVersioningStatus()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -526,14 +578,22 @@ const downloadMultiple = async () => {
|
||||
message.success(t('Download ready'))
|
||||
}
|
||||
|
||||
const confirmDelete = (keys: string[]) => {
|
||||
dialog.error({
|
||||
title: t('Warning'),
|
||||
content: t('Are you sure you want to delete the selected objects?'),
|
||||
positiveText: t('Confirm'),
|
||||
negativeText: t('Cancel'),
|
||||
onPositiveClick: () => handleDelete(keys),
|
||||
})
|
||||
const openDeleteDialog = (keys: string[]) => {
|
||||
deleteDialogKeys.value = keys
|
||||
deleteAllVersions.value = false
|
||||
deleteDialogOpen.value = true
|
||||
}
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
const keys = [...deleteDialogKeys.value]
|
||||
deleteDialogOpen.value = false
|
||||
if (!keys.length) return
|
||||
|
||||
if (bucketVersioningEnabled.value && deleteAllVersions.value) {
|
||||
await handleDeleteAllVersions(keys)
|
||||
} else {
|
||||
await handleDelete(keys)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (keys: string[]) => {
|
||||
@@ -552,12 +612,58 @@ const handleDelete = async (keys: string[]) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteAllVersions = async (keys: string[]) => {
|
||||
try {
|
||||
const targets = await collectKeysForDeletion(keys)
|
||||
if (!targets.length) {
|
||||
message.success(t('Delete Success'))
|
||||
} else {
|
||||
const items: { key: string; versionId?: string }[] = []
|
||||
|
||||
for (const key of targets) {
|
||||
if (!key) continue
|
||||
const versions = await objectApi.listObjectVersions(key)
|
||||
const versionItems = (versions.Versions || [])
|
||||
.filter(v => v.Key === key && v.VersionId)
|
||||
.map(v => ({
|
||||
key,
|
||||
versionId: v.VersionId!,
|
||||
}))
|
||||
const deleteMarkers = (versions.DeleteMarkers || [])
|
||||
.filter(m => m.Key === key && m.VersionId)
|
||||
.map(m => ({
|
||||
key,
|
||||
versionId: m.VersionId!,
|
||||
}))
|
||||
|
||||
if (versionItems.length === 0 && deleteMarkers.length === 0) {
|
||||
// 如果没有版本信息,退回到删除当前对象(等价于非版本桶)
|
||||
items.push({ key })
|
||||
} else {
|
||||
items.push(...versionItems, ...deleteMarkers)
|
||||
}
|
||||
}
|
||||
|
||||
if (!items.length) {
|
||||
message.success(t('Delete Success'))
|
||||
} else {
|
||||
;(taskStore as any).addDeleteVersionedItems(items, bucketName.value)
|
||||
message.success(t('Delete task created'))
|
||||
}
|
||||
}
|
||||
table.resetRowSelection()
|
||||
refresh()
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || t('Delete Failed'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
if (!checkedKeys.value.length) {
|
||||
message.warning(t('Please select at least one item'))
|
||||
return
|
||||
}
|
||||
confirmDelete([...checkedKeys.value])
|
||||
openDeleteDialog([...checkedKeys.value])
|
||||
}
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Modal v-model="visibleProxy" :title="t('Object Versions')" size="lg" :close-on-backdrop="false">
|
||||
<Modal v-model="visibleProxy" :title="t('Object Versions')" size="xl" :close-on-backdrop="false">
|
||||
<div class="space-y-4 z-20">
|
||||
<DataTable class="pb-4" :table="table" :is-loading="loading" :empty-title="t('No Versions')" />
|
||||
</div>
|
||||
|
||||
@@ -454,6 +454,7 @@
|
||||
"No Tasks": "Keine Aufgaben",
|
||||
"No Tiers": "Keine Tiers",
|
||||
"No Versions": "Keine Versionen",
|
||||
"Delete All Versions": "Alle Versionen löschen",
|
||||
"No bucket selected": "Kein Bucket ausgewählt",
|
||||
"No buckets found": "Keine Buckets gefunden",
|
||||
"No buckets match your search": "Keine Buckets entsprechen Ihrer Suche",
|
||||
|
||||
@@ -455,6 +455,7 @@
|
||||
"No Tasks": "No Tasks",
|
||||
"No Tiers": "No Tiers",
|
||||
"No Versions": "No Versions",
|
||||
"Delete All Versions": "Delete All Versions",
|
||||
"No bucket selected": "No bucket selected",
|
||||
"No buckets found": "No buckets found",
|
||||
"No buckets match your search": "No buckets match your search",
|
||||
|
||||
@@ -454,6 +454,7 @@
|
||||
"No Tasks": "Sin Tareas",
|
||||
"No Tiers": "Sin Niveles",
|
||||
"No Versions": "Sin Versiones",
|
||||
"Delete All Versions": "Eliminar Todas las Versiones",
|
||||
"No bucket selected": "No hay bucket seleccionado",
|
||||
"No buckets found": "No se encontraron buckets",
|
||||
"No buckets match your search": "No hay buckets que coincidan con su búsqueda",
|
||||
|
||||
@@ -454,6 +454,7 @@
|
||||
"No Tasks": "Aucune tâche",
|
||||
"No Tiers": "Aucun niveau",
|
||||
"No Versions": "Aucune version",
|
||||
"Delete All Versions": "Supprimer toutes les versions",
|
||||
"No bucket selected": "Aucun compartiment sélectionné",
|
||||
"No buckets found": "Aucun compartiment trouvé",
|
||||
"No buckets match your search": "Aucun compartiment ne correspond à votre recherche",
|
||||
|
||||
@@ -454,6 +454,7 @@
|
||||
"No Tasks": "Tidak Ada Tugas",
|
||||
"No Tiers": "Tidak Ada Tier",
|
||||
"No Versions": "Tidak Ada Versi",
|
||||
"Delete All Versions": "Hapus Semua Versi",
|
||||
"No bucket selected": "Tidak ada bucket yang dipilih",
|
||||
"No buckets found": "Tidak ada bucket ditemukan",
|
||||
"No buckets match your search": "Tidak ada bucket yang cocok dengan pencarian Anda",
|
||||
|
||||
@@ -454,6 +454,7 @@
|
||||
"No Tasks": "Nessuna attività",
|
||||
"No Tiers": "Nessun tier",
|
||||
"No Versions": "Nessuna versione",
|
||||
"Delete All Versions": "Elimina tutte le versioni",
|
||||
"No bucket selected": "Nessun bucket selezionato",
|
||||
"No buckets found": "Nessun bucket trovato",
|
||||
"No buckets match your search": "Nessun bucket corrisponde alla tua ricerca",
|
||||
|
||||
@@ -454,6 +454,7 @@
|
||||
"No Tasks": "タスクなし",
|
||||
"No Tiers": "ティアなし",
|
||||
"No Versions": "バージョンなし",
|
||||
"Delete All Versions": "すべてのバージョンを削除",
|
||||
"No bucket selected": "バケットが選択されていません",
|
||||
"No buckets found": "バケットが見つかりません",
|
||||
"No buckets match your search": "検索に一致するバケットがありません",
|
||||
|
||||
@@ -454,6 +454,7 @@
|
||||
"No Tasks": "작업 없음",
|
||||
"No Tiers": "티어 없음",
|
||||
"No Versions": "버전 없음",
|
||||
"Delete All Versions": "모든 버전 삭제",
|
||||
"No bucket selected": "버킷이 선택되지 않음",
|
||||
"No buckets found": "버킷을 찾을 수 없음",
|
||||
"No buckets match your search": "검색과 일치하는 버킷이 없음",
|
||||
|
||||
@@ -454,6 +454,7 @@
|
||||
"No Tasks": "Sem Tarefas",
|
||||
"No Tiers": "Sem Tiers",
|
||||
"No Versions": "Sem Versões",
|
||||
"Delete All Versions": "Excluir Todas as Versões",
|
||||
"No bucket selected": "Nenhum bucket selecionado",
|
||||
"No buckets found": "Nenhum bucket encontrado",
|
||||
"No buckets match your search": "Nenhum bucket corresponde à sua pesquisa",
|
||||
|
||||
@@ -454,6 +454,7 @@
|
||||
"No Tasks": "Нет задач",
|
||||
"No Tiers": "Нет уровней",
|
||||
"No Versions": "Нет версий",
|
||||
"Delete All Versions": "Удалить все версии",
|
||||
"No bucket selected": "Бакет не выбран",
|
||||
"No buckets found": "Бакеты не найдены",
|
||||
"No buckets match your search": "Нет бакетов, соответствующих вашему поиску",
|
||||
|
||||
@@ -454,6 +454,7 @@
|
||||
"No Tasks": "Görev Yok",
|
||||
"No Tiers": "No Tiers",
|
||||
"No Versions": "No Versions",
|
||||
"Delete All Versions": "Delete All Versions",
|
||||
"No bucket selected": "No bucket selected",
|
||||
"No buckets found": "No buckets found",
|
||||
"No buckets match your search": "No buckets match your search",
|
||||
|
||||
@@ -456,6 +456,7 @@
|
||||
"No Tasks": "暂无任务",
|
||||
"No Tiers": "无存储层",
|
||||
"No Versions": "无版本",
|
||||
"Delete All Versions": "删除所有版本",
|
||||
"No bucket selected": "未选择存储桶",
|
||||
"No buckets found": "未找到存储桶",
|
||||
"No buckets match your search": "没有存储桶匹配您的搜索",
|
||||
|
||||
+34
-8
@@ -8,6 +8,11 @@ export interface DeleteTask extends ManagedTask<DeleteStatus> {
|
||||
key: string
|
||||
bucketName: string
|
||||
prefix?: string
|
||||
/**
|
||||
* Optional object version id.
|
||||
* When provided, the delete operation will target the specific version.
|
||||
*/
|
||||
versionId?: string
|
||||
actionLabel: string
|
||||
displayName: string
|
||||
subInfo: string
|
||||
@@ -21,6 +26,11 @@ export interface DeleteTaskConfig {
|
||||
export interface DeleteTaskHelpers {
|
||||
handler: TaskHandler<DeleteTask, DeleteStatus>
|
||||
createTasks: (keys: string[], bucketName: string, prefix?: string) => DeleteTask[]
|
||||
createVersionedTasks: (
|
||||
items: { key: string; versionId?: string }[],
|
||||
bucketName: string,
|
||||
prefix?: string
|
||||
) => DeleteTask[]
|
||||
}
|
||||
|
||||
const lifecycle: TaskLifecycleStatus<DeleteStatus> = {
|
||||
@@ -36,13 +46,14 @@ export const createDeleteTaskHelpers = (s3Client: S3Client, config: DeleteTaskCo
|
||||
const retryDelay = config.retryDelay ?? 1000
|
||||
|
||||
const perform: TaskHandler<DeleteTask, DeleteStatus>['perform'] = async task => {
|
||||
const { key, bucketName, prefix } = task
|
||||
const { key, bucketName, prefix, versionId } = task
|
||||
const abortController = new AbortController()
|
||||
task.abortController = abortController
|
||||
|
||||
const command = new DeleteObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: (prefix || '') + key,
|
||||
...(versionId ? { VersionId: versionId } : {}),
|
||||
})
|
||||
|
||||
await s3Client.send(command, { abortSignal: abortController.signal })
|
||||
@@ -66,24 +77,39 @@ export const createDeleteTaskHelpers = (s3Client: S3Client, config: DeleteTaskCo
|
||||
retryDelay,
|
||||
}
|
||||
|
||||
const createTasks = (keys: string[], bucketName: string, prefix?: string) =>
|
||||
keys.map(key => ({
|
||||
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${key}`,
|
||||
const createTasksFromItems = (
|
||||
items: { key: string; versionId?: string }[],
|
||||
bucketName: string,
|
||||
prefix?: string
|
||||
): DeleteTask[] =>
|
||||
items.map(item => ({
|
||||
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${item.key}-${item.versionId ?? 'latest'}`,
|
||||
kind: 'delete' as const,
|
||||
key,
|
||||
key: item.key,
|
||||
versionId: item.versionId,
|
||||
status: lifecycle.pending,
|
||||
progress: 0,
|
||||
bucketName,
|
||||
prefix: prefix ?? '',
|
||||
actionLabel: 'Delete',
|
||||
displayName: key,
|
||||
subInfo: '',
|
||||
displayName: item.key,
|
||||
subInfo: item.versionId ? `Version: ${item.versionId}` : '',
|
||||
error: undefined,
|
||||
abortController: undefined,
|
||||
retryCount: 0,
|
||||
}))
|
||||
|
||||
return { handler, createTasks }
|
||||
const createTasks = (keys: string[], bucketName: string, prefix?: string) =>
|
||||
createTasksFromItems(
|
||||
keys.map(key => ({ key })),
|
||||
bucketName,
|
||||
prefix
|
||||
)
|
||||
|
||||
const createVersionedTasks = (items: { key: string; versionId?: string }[], bucketName: string, prefix?: string) =>
|
||||
createTasksFromItems(items, bucketName, prefix)
|
||||
|
||||
return { handler, createTasks, createVersionedTasks }
|
||||
}
|
||||
|
||||
export type {
|
||||
|
||||
@@ -83,6 +83,11 @@ export const useTaskManagerStore = defineStore('taskManager', {
|
||||
this.taskManager.enqueue(newTasks)
|
||||
},
|
||||
|
||||
addDeleteVersionedItems(items: { key: string; versionId?: string }[], bucketName: string, prefix?: string) {
|
||||
const newTasks = this.deleteHelpers.createVersionedTasks(items, bucketName, prefix)
|
||||
this.taskManager.enqueue(newTasks)
|
||||
},
|
||||
|
||||
// 通用控制
|
||||
cancelTask(taskId: string) {
|
||||
this.taskManager.cancelTask(taskId)
|
||||
|
||||
Reference in New Issue
Block a user