mirror of
https://github.com/rustfs/console.git
synced 2026-08-31 01:37:52 +08:00
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:
+67
-57
@@ -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,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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -445,6 +445,7 @@
|
||||
"No KMS keys found": "未找到KMS密钥",
|
||||
"No License": "无许可证",
|
||||
"No Objects": "无对象",
|
||||
"Show Deleted Objects": "显示已删除的对象",
|
||||
"No Policies": "无策略",
|
||||
"No Selection": "未选择文件/文件夹",
|
||||
"No Tasks": "暂无任务",
|
||||
|
||||
Reference in New Issue
Block a user