feat: add option to show deleted objects in object list

- Add checkbox to toggle showing deleted objects
- Update UI layout to include search and show deleted option
- Add i18n translations for 'Show Deleted Objects' in en-US, tr-TR, and zh-CN
This commit is contained in:
overtrue
2025-11-20 19:57:50 +08:00
parent 2eec6ac19c
commit 74cfe21316
5 changed files with 71 additions and 58 deletions
+67 -57
View File
@@ -1,7 +1,13 @@
<template>
<div class="space-y-6">
<page-header>
<SearchInput v-model="searchTerm" :placeholder="t('Search')" clearable />
<div class="flex flex-wrap items-center gap-4 min-w-[40vw]">
<SearchInput v-model="searchTerm" :placeholder="t('Search')" clearable class="lg:max-w-sm" />
<label class="flex items-center gap-2 text-sm text-muted-foreground">
<Checkbox v-model="showDeleted" />
<span>{{ t('Show Deleted Objects') }}</span>
</label>
</div>
<template #actions>
<object-upload-stats />
<object-delete-stats />
@@ -13,13 +19,7 @@
<Icon name="ri:file-add-line" class="size-4" />
<span>{{ t('Upload File') }}/{{ t('Folder') }}</span>
</Button>
<Button
variant="outline"
class="text-destructive border-destructive"
:disabled="!checkedKeys.length"
v-show="checkedKeys.length"
@click="handleBatchDelete"
>
<Button variant="outline" class="text-destructive border-destructive" :disabled="!checkedKeys.length" v-show="checkedKeys.length" @click="handleBatchDelete">
<Icon name="ri:delete-bin-5-line" class="size-4" />
<span>{{ t('Delete Selected') }}</span>
</Button>
@@ -34,12 +34,7 @@
</template>
</page-header>
<DataTable
:table="table"
:is-loading="loading"
:empty-title="t('No Objects')"
:empty-description="t('Upload files or create folders to populate this bucket.')"
/>
<DataTable :table="table" :is-loading="loading" :empty-title="t('No Objects')" :empty-description="t('Upload files or create folders to populate this bucket.')" />
<div class="flex justify-end gap-2">
<Button variant="outline" :disabled="!continuationToken" @click="goToPreviousPage">
@@ -53,39 +48,32 @@
</div>
</div>
<object-upload-picker
:show="uploadPickerVisible"
:bucketName="bucketName"
:prefix="prefix"
@update:show="
val => {
uploadPickerVisible = val
refresh()
}
"
/>
<object-new-form
:show="newObjectFormVisible"
:bucketName="bucketName"
:prefix="prefix"
:asPrefix="newObjectAsPrefix"
@update:show="
val => {
newObjectFormVisible = val
refresh()
}
"
/>
<object-upload-picker :show="uploadPickerVisible" :bucketName="bucketName" :prefix="prefix" @update:show="
val => {
uploadPickerVisible = val
refresh()
}
" />
<object-new-form :show="newObjectFormVisible" :bucketName="bucketName" :prefix="prefix" :asPrefix="newObjectAsPrefix" @update:show="
val => {
newObjectFormVisible = val
refresh()
}
" />
<object-info ref="infoRef" :bucket-name="bucketName" @refresh-parent="handleObjectDeleted" />
</template>
<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { useLocalStorage } from '@vueuse/core'
import { Icon, NuxtLink } from '#components'
import DataTable from '@/components/data-table/data-table.vue'
import { useDataTable } from '@/components/data-table/useDataTable'
import { ListObjectsV2Command } from '@aws-sdk/client-s3'
import type { HttpRequest } from '@smithy/protocol-http'
import type { ColumnDef } from '@tanstack/vue-table'
import dayjs from 'dayjs'
import { saveAs } from 'file-saver'
@@ -107,6 +95,7 @@ const newObjectFormVisible = ref(false)
const newObjectAsPrefix = ref(false)
const searchTerm = ref('')
const loading = ref(false)
const showDeleted = useLocalStorage('object-list-show-deleted', false)
const debounce = (fn: Function, delay: number) => {
let timer: NodeJS.Timeout | null = null
@@ -172,15 +161,26 @@ const handleObjectDeleted = () => {
const fetchObjects = async (): Promise<ObjectRow[]> => {
loading.value = true
try {
const response = await $s3Client.send(
new ListObjectsV2Command({
Bucket: bucketName.value,
Prefix: prefix.value,
Delimiter: '/',
ContinuationToken: continuationToken.value,
MaxKeys: pageSize.value,
})
)
const command = new ListObjectsV2Command({
Bucket: bucketName.value,
Prefix: prefix.value,
Delimiter: '/',
ContinuationToken: continuationToken.value,
MaxKeys: pageSize.value,
})
if (showDeleted.value) {
command.middlewareStack.add(
next => async args => {
const request = args.request as HttpRequest
request.headers['X-Rustfs-Include-Deleted'] = 'true'
return next(args)
},
{ step: 'build', name: 'includeDeletedObjectsMiddleware', tags: ['INCLUDE_DELETED'] }
)
}
const response = await $s3Client.send(command)
nextToken.value = response.NextContinuationToken
@@ -200,12 +200,16 @@ const fetchObjects = async (): Promise<ObjectRow[]> => {
return [...prefixItems, ...objectItems]
} finally {
loading.value = false
// 200ms delay to avoid flickering
setTimeout(() => {
loading.value = false
}, 200)
}
}
const asyncDataCacheKey = computed(() => {
return `objects-${bucketName.value}-${prefix.value}-${continuationToken.value || 'start'}-${searchTerm.value || 'all'}`
return `objects-${bucketName.value}-${prefix.value}-${continuationToken.value || 'start'}-${searchTerm.value || 'all'}-${showDeleted.value ? 'withDeleted' : 'withoutDeleted'
}`
})
const displayKey = (key: string) => {
@@ -229,10 +233,16 @@ const { data, refresh } = await useAsyncData<ObjectRow[]>(
},
{
default: () => [],
watch: [bucketName, prefix, continuationToken, searchTerm],
watch: [bucketName, prefix, continuationToken, searchTerm, showDeleted],
}
)
watch(showDeleted, () => {
continuationToken.value = undefined
tokenHistory.value = []
refresh()
})
const columns = computed<ColumnDef<ObjectRow, any>[]>(() => {
return [
{
@@ -285,14 +295,14 @@ const columns = computed<ColumnDef<ObjectRow, any>[]>(() => {
h('div', { class: 'flex items-center gap-2' }, [
row.original.type === 'object'
? h(
Button,
{
variant: 'outline',
size: 'sm',
onClick: () => downloadFile(row.original.Key),
},
() => [h(Icon, { name: 'ri:download-cloud-2-line', class: 'size-4' }), h('span', t('Download'))]
)
Button,
{
variant: 'outline',
size: 'sm',
onClick: () => downloadFile(row.original.Key),
},
() => [h(Icon, { name: 'ri:download-cloud-2-line', class: 'size-4' }), h('span', t('Download'))]
)
: null,
h(
Button,
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<div class="sticky bg-background top-0 z-10 flex flex-col justify-between gap-2 md:flex-row">
<div class="sticky bg-background top-0 z-10 flex flex-col justify-between gap-2 lg:flex-row">
<div class="space-y-2">
<slot />
<slot name="description"></slot>
+1
View File
@@ -444,6 +444,7 @@
"No KMS keys found": "No KMS keys found",
"No License": "No License",
"No Objects": "No Objects",
"Show Deleted Objects": "Show Deleted Objects",
"No Policies": "No Policies",
"No Selection": "No Selection",
"No Tasks": "No Tasks",
+1
View File
@@ -444,6 +444,7 @@
"No KMS keys found": "KMS anahtarı bulunamadı",
"No License": "No License",
"No Objects": "No Objects",
"Show Deleted Objects": "Show Deleted Objects",
"No Policies": "No Policies",
"No Selection": "Seçim Yok",
"No Tasks": "Görev Yok",
+1
View File
@@ -445,6 +445,7 @@
"No KMS keys found": "未找到KMS密钥",
"No License": "无许可证",
"No Objects": "无对象",
"Show Deleted Objects": "显示已删除的对象",
"No Policies": "无策略",
"No Selection": "未选择文件/文件夹",
"No Tasks": "暂无任务",