Skip to content

Commit 74cfe21

Browse files
committed
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
1 parent 2eec6ac commit 74cfe21

5 files changed

Lines changed: 71 additions & 58 deletions

File tree

‎components/object/list.vue‎

Lines changed: 67 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
<template>
22
<div class="space-y-6">
33
<page-header>
4-
<SearchInput v-model="searchTerm" :placeholder="t('Search')" clearable />
4+
<div class="flex flex-wrap items-center gap-4 min-w-[40vw]">
5+
<SearchInput v-model="searchTerm" :placeholder="t('Search')" clearable class="lg:max-w-sm" />
6+
<label class="flex items-center gap-2 text-sm text-muted-foreground">
7+
<Checkbox v-model="showDeleted" />
8+
<span>{{ t('Show Deleted Objects') }}</span>
9+
</label>
10+
</div>
511
<template #actions>
612
<object-upload-stats />
713
<object-delete-stats />
@@ -13,13 +19,7 @@
1319
<Icon name="ri:file-add-line" class="size-4" />
1420
<span>{{ t('Upload File') }}/{{ t('Folder') }}</span>
1521
</Button>
16-
<Button
17-
variant="outline"
18-
class="text-destructive border-destructive"
19-
:disabled="!checkedKeys.length"
20-
v-show="checkedKeys.length"
21-
@click="handleBatchDelete"
22-
>
22+
<Button variant="outline" class="text-destructive border-destructive" :disabled="!checkedKeys.length" v-show="checkedKeys.length" @click="handleBatchDelete">
2323
<Icon name="ri:delete-bin-5-line" class="size-4" />
2424
<span>{{ t('Delete Selected') }}</span>
2525
</Button>
@@ -34,12 +34,7 @@
3434
</template>
3535
</page-header>
3636

37-
<DataTable
38-
:table="table"
39-
:is-loading="loading"
40-
:empty-title="t('No Objects')"
41-
:empty-description="t('Upload files or create folders to populate this bucket.')"
42-
/>
37+
<DataTable :table="table" :is-loading="loading" :empty-title="t('No Objects')" :empty-description="t('Upload files or create folders to populate this bucket.')" />
4338

4439
<div class="flex justify-end gap-2">
4540
<Button variant="outline" :disabled="!continuationToken" @click="goToPreviousPage">
@@ -53,39 +48,32 @@
5348
</div>
5449
</div>
5550

56-
<object-upload-picker
57-
:show="uploadPickerVisible"
58-
:bucketName="bucketName"
59-
:prefix="prefix"
60-
@update:show="
61-
val => {
62-
uploadPickerVisible = val
63-
refresh()
64-
}
65-
"
66-
/>
67-
<object-new-form
68-
:show="newObjectFormVisible"
69-
:bucketName="bucketName"
70-
:prefix="prefix"
71-
:asPrefix="newObjectAsPrefix"
72-
@update:show="
73-
val => {
74-
newObjectFormVisible = val
75-
refresh()
76-
}
77-
"
78-
/>
51+
<object-upload-picker :show="uploadPickerVisible" :bucketName="bucketName" :prefix="prefix" @update:show="
52+
val => {
53+
uploadPickerVisible = val
54+
refresh()
55+
}
56+
" />
57+
<object-new-form :show="newObjectFormVisible" :bucketName="bucketName" :prefix="prefix" :asPrefix="newObjectAsPrefix" @update:show="
58+
val => {
59+
newObjectFormVisible = val
60+
refresh()
61+
}
62+
" />
7963
<object-info ref="infoRef" :bucket-name="bucketName" @refresh-parent="handleObjectDeleted" />
8064
</template>
8165

8266
<script setup lang="ts">
8367
import { Button } from '@/components/ui/button'
68+
import { Checkbox } from '@/components/ui/checkbox'
69+
70+
import { useLocalStorage } from '@vueuse/core'
8471
8572
import { Icon, NuxtLink } from '#components'
8673
import DataTable from '@/components/data-table/data-table.vue'
8774
import { useDataTable } from '@/components/data-table/useDataTable'
8875
import { ListObjectsV2Command } from '@aws-sdk/client-s3'
76+
import type { HttpRequest } from '@smithy/protocol-http'
8977
import type { ColumnDef } from '@tanstack/vue-table'
9078
import dayjs from 'dayjs'
9179
import { saveAs } from 'file-saver'
@@ -107,6 +95,7 @@ const newObjectFormVisible = ref(false)
10795
const newObjectAsPrefix = ref(false)
10896
const searchTerm = ref('')
10997
const loading = ref(false)
98+
const showDeleted = useLocalStorage('object-list-show-deleted', false)
11099
111100
const debounce = (fn: Function, delay: number) => {
112101
let timer: NodeJS.Timeout | null = null
@@ -172,15 +161,26 @@ const handleObjectDeleted = () => {
172161
const fetchObjects = async (): Promise<ObjectRow[]> => {
173162
loading.value = true
174163
try {
175-
const response = await $s3Client.send(
176-
new ListObjectsV2Command({
177-
Bucket: bucketName.value,
178-
Prefix: prefix.value,
179-
Delimiter: '/',
180-
ContinuationToken: continuationToken.value,
181-
MaxKeys: pageSize.value,
182-
})
183-
)
164+
const command = new ListObjectsV2Command({
165+
Bucket: bucketName.value,
166+
Prefix: prefix.value,
167+
Delimiter: '/',
168+
ContinuationToken: continuationToken.value,
169+
MaxKeys: pageSize.value,
170+
})
171+
172+
if (showDeleted.value) {
173+
command.middlewareStack.add(
174+
next => async args => {
175+
const request = args.request as HttpRequest
176+
request.headers['X-Rustfs-Include-Deleted'] = 'true'
177+
return next(args)
178+
},
179+
{ step: 'build', name: 'includeDeletedObjectsMiddleware', tags: ['INCLUDE_DELETED'] }
180+
)
181+
}
182+
183+
const response = await $s3Client.send(command)
184184
185185
nextToken.value = response.NextContinuationToken
186186
@@ -200,12 +200,16 @@ const fetchObjects = async (): Promise<ObjectRow[]> => {
200200
201201
return [...prefixItems, ...objectItems]
202202
} finally {
203-
loading.value = false
203+
// 200ms delay to avoid flickering
204+
setTimeout(() => {
205+
loading.value = false
206+
}, 200)
204207
}
205208
}
206209
207210
const asyncDataCacheKey = computed(() => {
208-
return `objects-${bucketName.value}-${prefix.value}-${continuationToken.value || 'start'}-${searchTerm.value || 'all'}`
211+
return `objects-${bucketName.value}-${prefix.value}-${continuationToken.value || 'start'}-${searchTerm.value || 'all'}-${showDeleted.value ? 'withDeleted' : 'withoutDeleted'
212+
}`
209213
})
210214
211215
const displayKey = (key: string) => {
@@ -229,10 +233,16 @@ const { data, refresh } = await useAsyncData<ObjectRow[]>(
229233
},
230234
{
231235
default: () => [],
232-
watch: [bucketName, prefix, continuationToken, searchTerm],
236+
watch: [bucketName, prefix, continuationToken, searchTerm, showDeleted],
233237
}
234238
)
235239
240+
watch(showDeleted, () => {
241+
continuationToken.value = undefined
242+
tokenHistory.value = []
243+
refresh()
244+
})
245+
236246
const columns = computed<ColumnDef<ObjectRow, any>[]>(() => {
237247
return [
238248
{
@@ -285,14 +295,14 @@ const columns = computed<ColumnDef<ObjectRow, any>[]>(() => {
285295
h('div', { class: 'flex items-center gap-2' }, [
286296
row.original.type === 'object'
287297
? h(
288-
Button,
289-
{
290-
variant: 'outline',
291-
size: 'sm',
292-
onClick: () => downloadFile(row.original.Key),
293-
},
294-
() => [h(Icon, { name: 'ri:download-cloud-2-line', class: 'size-4' }), h('span', t('Download'))]
295-
)
298+
Button,
299+
{
300+
variant: 'outline',
301+
size: 'sm',
302+
onClick: () => downloadFile(row.original.Key),
303+
},
304+
() => [h(Icon, { name: 'ri:download-cloud-2-line', class: 'size-4' }), h('span', t('Download'))]
305+
)
296306
: null,
297307
h(
298308
Button,

‎components/page-header.vue‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<div class="sticky bg-background top-0 z-10 flex flex-col justify-between gap-2 md:flex-row">
2+
<div class="sticky bg-background top-0 z-10 flex flex-col justify-between gap-2 lg:flex-row">
33
<div class="space-y-2">
44
<slot />
55
<slot name="description"></slot>

‎i18n/locales/en-US.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@
444444
"No KMS keys found": "No KMS keys found",
445445
"No License": "No License",
446446
"No Objects": "No Objects",
447+
"Show Deleted Objects": "Show Deleted Objects",
447448
"No Policies": "No Policies",
448449
"No Selection": "No Selection",
449450
"No Tasks": "No Tasks",

‎i18n/locales/tr-TR.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@
444444
"No KMS keys found": "KMS anahtarı bulunamadı",
445445
"No License": "No License",
446446
"No Objects": "No Objects",
447+
"Show Deleted Objects": "Show Deleted Objects",
447448
"No Policies": "No Policies",
448449
"No Selection": "Seçim Yok",
449450
"No Tasks": "Görev Yok",

‎i18n/locales/zh-CN.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@
445445
"No KMS keys found": "未找到KMS密钥",
446446
"No License": "无许可证",
447447
"No Objects": "无对象",
448+
"Show Deleted Objects": "显示已删除的对象",
448449
"No Policies": "无策略",
449450
"No Selection": "未选择文件/文件夹",
450451
"No Tasks": "暂无任务",

0 commit comments

Comments
 (0)