mirror of
https://github.com/rustfs/console.git
synced 2026-08-29 03:52:28 +08:00
fix: show all object versions in dialog (#154)
This commit is contained in:
@@ -6,6 +6,7 @@ import { RiFileCopyLine, RiEyeLine, RiDownloadCloud2Line, RiDeleteBin5Line, RiLo
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Spinner } from "@/components/ui/spinner"
|
||||
import { DataTable } from "@/components/data-table/data-table"
|
||||
import { useDataTable } from "@/hooks/use-data-table"
|
||||
import { useObject } from "@/hooks/use-object"
|
||||
@@ -55,6 +56,7 @@ export function ObjectVersions({
|
||||
|
||||
const [versions, setVersions] = React.useState<VersionRow[]>([])
|
||||
const [loading, setLoading] = React.useState(false)
|
||||
const [deletingVersionId, setDeletingVersionId] = React.useState<string | null>(null)
|
||||
|
||||
const fetchVersions = React.useCallback(async () => {
|
||||
if (!objectKey) return
|
||||
@@ -123,13 +125,16 @@ export function ObjectVersions({
|
||||
async (row: VersionRow) => {
|
||||
const versionId = row.VersionId
|
||||
if (!versionId) return
|
||||
setDeletingVersionId(versionId)
|
||||
try {
|
||||
await deleteObject(objectKey, versionId)
|
||||
message.success(t("Delete Success"))
|
||||
void fetchVersions()
|
||||
await fetchVersions()
|
||||
onRefreshParent()
|
||||
} catch (err) {
|
||||
message.error((err as Error)?.message ?? t("Delete Failed"))
|
||||
} finally {
|
||||
setDeletingVersionId(null)
|
||||
}
|
||||
},
|
||||
[deleteObject, objectKey, message, t, fetchVersions, onRefreshParent],
|
||||
@@ -251,10 +256,14 @@ export function ObjectVersions({
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="text-white"
|
||||
disabled={deletingVersionId === row.original.VersionId}
|
||||
onClick={() => deleteVersion(row.original)}
|
||||
>
|
||||
<RiDeleteBin5Line className="size-4" aria-hidden />
|
||||
{deletingVersionId === row.original.VersionId ? (
|
||||
<Spinner className="size-4" aria-hidden />
|
||||
) : (
|
||||
<RiDeleteBin5Line className="size-4" aria-hidden />
|
||||
)}
|
||||
{t("Delete")}
|
||||
</Button>
|
||||
) : null}
|
||||
@@ -264,12 +273,24 @@ export function ObjectVersions({
|
||||
meta: { minWidth: 360 },
|
||||
},
|
||||
],
|
||||
[t, onPreview, copyVersionId, restoreVersion, downloadVersion, deleteVersion, canCapability, bucketName, objectKey],
|
||||
[
|
||||
t,
|
||||
onPreview,
|
||||
copyVersionId,
|
||||
restoreVersion,
|
||||
downloadVersion,
|
||||
deleteVersion,
|
||||
canCapability,
|
||||
bucketName,
|
||||
objectKey,
|
||||
deletingVersionId,
|
||||
],
|
||||
)
|
||||
|
||||
const { table } = useDataTable<VersionRow>({
|
||||
data: versions,
|
||||
columns,
|
||||
disablePagination: true,
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
+31
-12
@@ -4,13 +4,16 @@ import { useCallback } from "react"
|
||||
import {
|
||||
CopyObjectCommand,
|
||||
DeleteObjectCommand,
|
||||
type DeleteMarkerEntry,
|
||||
GetObjectLegalHoldCommand,
|
||||
GetObjectRetentionCommand,
|
||||
GetObjectTaggingCommand,
|
||||
GetObjectCommand,
|
||||
HeadObjectCommand,
|
||||
ListObjectVersionsCommand,
|
||||
type ListObjectVersionsCommandOutput,
|
||||
ListObjectsV2Command,
|
||||
type ObjectVersion,
|
||||
ObjectLockRetentionMode,
|
||||
PutObjectCommand,
|
||||
PutObjectLegalHoldCommand,
|
||||
@@ -291,20 +294,36 @@ export function useObject(bucket: string) {
|
||||
|
||||
const listObjectVersions = useCallback(
|
||||
async (key: string) => {
|
||||
const res = await client.send(
|
||||
new ListObjectVersionsCommand({
|
||||
Bucket: bucket,
|
||||
Prefix: key,
|
||||
Delimiter: "/",
|
||||
EncodingType: "url",
|
||||
}),
|
||||
)
|
||||
decodeS3UrlEncodedObjectVersions(res)
|
||||
const Versions = (res.Versions ?? []).filter((v) => v.Key === key)
|
||||
const DeleteMarkers = (res.DeleteMarkers ?? []).filter((m) => m.Key === key)
|
||||
const Versions: ObjectVersion[] = []
|
||||
const DeleteMarkers: DeleteMarkerEntry[] = []
|
||||
let keyMarker: string | undefined
|
||||
let versionIdMarker: string | undefined
|
||||
let isTruncated = true
|
||||
let lastResponse: Partial<ListObjectVersionsCommandOutput> = {}
|
||||
|
||||
while (isTruncated) {
|
||||
const res = await client.send(
|
||||
new ListObjectVersionsCommand({
|
||||
Bucket: bucket,
|
||||
Prefix: key,
|
||||
Delimiter: "/",
|
||||
EncodingType: "url",
|
||||
KeyMarker: keyMarker,
|
||||
VersionIdMarker: versionIdMarker,
|
||||
}),
|
||||
)
|
||||
decodeS3UrlEncodedObjectVersions(res)
|
||||
lastResponse = res
|
||||
Versions.push(...(res.Versions ?? []).filter((v) => v.Key === key))
|
||||
DeleteMarkers.push(...(res.DeleteMarkers ?? []).filter((m) => m.Key === key))
|
||||
isTruncated = res.IsTruncated ?? false
|
||||
keyMarker = res.NextKeyMarker
|
||||
versionIdMarker = res.NextVersionIdMarker
|
||||
}
|
||||
|
||||
Versions.sort((a, b) => new Date(b.LastModified!).getTime() - new Date(a.LastModified!).getTime())
|
||||
DeleteMarkers.sort((a, b) => new Date(b.LastModified!).getTime() - new Date(a.LastModified!).getTime())
|
||||
return { ...res, Versions, DeleteMarkers }
|
||||
return { ...lastResponse, Versions, DeleteMarkers, IsTruncated: false }
|
||||
},
|
||||
[client, bucket],
|
||||
)
|
||||
|
||||
@@ -26,3 +26,29 @@ test("object versions dialog is wide, sortable by date and size, and shows summa
|
||||
assert.match(source, /accessorFn: \(row\) => \(row\.LastModified \? new Date\(row\.LastModified\)\.getTime\(\) : 0\)/)
|
||||
assert.match(source, /accessorFn: \(row\) => row\.Size \?\? 0/)
|
||||
})
|
||||
|
||||
test("object versions dialog does not hide versions behind unreachable default pagination", () => {
|
||||
const source = fs.readFileSync("components/object/versions.tsx", "utf8")
|
||||
|
||||
assert.match(source, /disablePagination: true/)
|
||||
})
|
||||
|
||||
test("object versions delete button reflects the active delete request", () => {
|
||||
const source = fs.readFileSync("components/object/versions.tsx", "utf8")
|
||||
|
||||
assert.match(source, /const \[deletingVersionId, setDeletingVersionId\] = React\.useState<string \| null>\(null\)/)
|
||||
assert.match(source, /disabled=\{deletingVersionId === row\.original\.VersionId\}/)
|
||||
assert.doesNotMatch(source, /variant="destructive"\s+size="sm"\s+className="text-white"/)
|
||||
})
|
||||
|
||||
test("object version listing follows S3 version pagination markers", () => {
|
||||
const source = fs.readFileSync("hooks/use-object.ts", "utf8")
|
||||
|
||||
assert.match(source, /let keyMarker: string \| undefined/)
|
||||
assert.match(source, /let versionIdMarker: string \| undefined/)
|
||||
assert.match(source, /KeyMarker: keyMarker/)
|
||||
assert.match(source, /VersionIdMarker: versionIdMarker/)
|
||||
assert.match(source, /keyMarker = res\.NextKeyMarker/)
|
||||
assert.match(source, /versionIdMarker = res\.NextVersionIdMarker/)
|
||||
assert.match(source, /while \(isTruncated\)/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user