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 />
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 >
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 " >
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">
8367import { Button } from ' @/components/ui/button'
68+ import { Checkbox } from ' @/components/ui/checkbox'
69+
70+ import { useLocalStorage } from ' @vueuse/core'
8471
8572import { Icon , NuxtLink } from ' #components'
8673import DataTable from ' @/components/data-table/data-table.vue'
8774import { useDataTable } from ' @/components/data-table/useDataTable'
8875import { ListObjectsV2Command } from ' @aws-sdk/client-s3'
76+ import type { HttpRequest } from ' @smithy/protocol-http'
8977import type { ColumnDef } from ' @tanstack/vue-table'
9078import dayjs from ' dayjs'
9179import { saveAs } from ' file-saver'
@@ -107,6 +95,7 @@ const newObjectFormVisible = ref(false)
10795const newObjectAsPrefix = ref (false )
10896const searchTerm = ref (' ' )
10997const loading = ref (false )
98+ const showDeleted = useLocalStorage (' object-list-show-deleted' , false )
11099
111100const debounce = (fn : Function , delay : number ) => {
112101 let timer: NodeJS .Timeout | null = null
@@ -172,15 +161,26 @@ const handleObjectDeleted = () => {
172161const 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
207210const 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
211215const 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+
236246const 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 ,
0 commit comments