Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 72 additions & 5 deletions components/object/versions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import * as React from "react"
import { useTranslation } from "react-i18next"
import { RiFileCopyLine, RiEyeLine, RiDownloadCloud2Line, RiDeleteBin5Line } from "@remixicon/react"
import { RiFileCopyLine, RiEyeLine, RiDownloadCloud2Line, RiDeleteBin5Line, RiLoopLeftLine } from "@remixicon/react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { DataTable } from "@/components/data-table/data-table"
import { useDataTable } from "@/hooks/use-data-table"
import { useObject } from "@/hooks/use-object"
import { usePermissions } from "@/hooks/use-permissions"
import { useDialog } from "@/lib/feedback/dialog"
import { useMessage } from "@/lib/feedback/message"
import { copyToClipboard } from "@/lib/clipboard"
import { exportFile } from "@/lib/export-file"
Expand All @@ -24,6 +26,7 @@ interface VersionRow {
LastModified?: Date
Size?: number
Key?: string
IsLatest?: boolean
}

interface ObjectVersionsProps {
Expand All @@ -45,7 +48,8 @@ export function ObjectVersions({
}: ObjectVersionsProps) {
const { t } = useTranslation()
const message = useMessage()
const { listObjectVersions, deleteObject } = useObject(bucketName)
const dialog = useDialog()
const { listObjectVersions, deleteObject, restoreObjectVersion } = useObject(bucketName)
const { canCapability } = usePermissions()
const client = useS3()

Expand Down Expand Up @@ -131,10 +135,46 @@ export function ObjectVersions({
[deleteObject, objectKey, message, t, fetchVersions, onRefreshParent],
)

const restoreVersion = React.useCallback(
(row: VersionRow) => {
const versionId = row.VersionId
if (!versionId || row.IsLatest) return

dialog.warning({
title: t("Restore Version"),
content: t("This will copy the selected historical version and make it the current object version."),
positiveText: t("Restore"),
negativeText: t("Cancel"),
onPositiveClick: async () => {
try {
await restoreObjectVersion(objectKey, versionId)
message.success(t("Restore Success"))
await fetchVersions()
onRefreshParent()
} catch (err) {
message.error((err as Error)?.message ?? t("Restore Failed"))
return false
}
},
})
},
[dialog, fetchVersions, message, objectKey, onRefreshParent, restoreObjectVersion, t],
)

const versionStats = React.useMemo(
() => ({
count: versions.length,
totalSize: versions.reduce((total, row) => total + (typeof row.Size === "number" ? row.Size : 0), 0),
}),
[versions],
)

const columns: ColumnDef<VersionRow>[] = React.useMemo(
() => [
{
id: "versionId",
accessorKey: "VersionId",
enableSorting: false,
header: () => t("VersionId"),
cell: ({ row }) => {
const versionId = row.original.VersionId ?? ""
Expand All @@ -153,22 +193,33 @@ export function ObjectVersions({
>
<RiFileCopyLine className="size-3" aria-hidden />
</Button>
{row.original.IsLatest ? (
<Badge variant="secondary" className="shrink-0">
{t("Current")}
</Badge>
) : null}
</div>
)
},
meta: { minWidth: 300 },
},
{
id: "lastModified",
accessorFn: (row) => (row.LastModified ? new Date(row.LastModified).getTime() : 0),
header: () => t("LastModified"),
cell: ({ row }) => (row.original.LastModified ? formatDateTime(row.original.LastModified) : ""),
meta: { width: 200 },
},
{
id: "size",
accessorFn: (row) => row.Size ?? 0,
header: () => t("Size"),
cell: ({ row }) => (typeof row.original.Size === "number" ? formatBytes(row.original.Size) : ""),
meta: { width: 140 },
},
{
id: "actions",
enableSorting: false,
header: () => t("Action"),
cell: ({ row }) => {
const objectContext = {
Expand All @@ -177,13 +228,19 @@ export function ObjectVersions({
}

return (
<div className="flex gap-2">
<div className="flex flex-wrap gap-2">
{canCapability("objects.version.view", objectContext) ? (
<Button variant="outline" size="sm" onClick={() => onPreview(row.original.VersionId ?? "")}>
<RiEyeLine className="size-4" aria-hidden />
{t("Preview")}
</Button>
) : null}
{!row.original.IsLatest && canCapability("objects.version.restore", objectContext) ? (
<Button variant="outline" size="sm" onClick={() => restoreVersion(row.original)}>
<RiLoopLeftLine className="size-4" aria-hidden />
{t("Restore")}
</Button>
) : null}
{canCapability("objects.download", objectContext) ? (
<Button variant="outline" size="sm" onClick={() => downloadVersion(row.original)}>
<RiDownloadCloud2Line className="size-4" aria-hidden />
Expand All @@ -204,9 +261,10 @@ export function ObjectVersions({
</div>
)
},
meta: { minWidth: 360 },
},
],
[t, onPreview, copyVersionId, downloadVersion, deleteVersion, canCapability, bucketName, objectKey],
[t, onPreview, copyVersionId, restoreVersion, downloadVersion, deleteVersion, canCapability, bucketName, objectKey],
)

const { table } = useDataTable<VersionRow>({
Expand All @@ -216,10 +274,19 @@ export function ObjectVersions({

return (
<Dialog open={visible} onOpenChange={(open) => !open && onClose()} disablePointerDismissal>
<DialogContent className="max-h-[80vh] overflow-y-auto overflow-x-hidden sm:max-w-4xl">
<DialogContent className="max-h-[80vh] w-[calc(100vw-2rem)] overflow-y-auto overflow-x-hidden sm:max-w-5xl 2xl:w-[80vw] 2xl:max-w-[80vw]">
<DialogHeader>
<DialogTitle>{t("Object Versions")}</DialogTitle>
</DialogHeader>
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-muted-foreground">
<span>
{t("Versions")}: <span className="font-medium text-foreground">{versionStats.count}</span>
</span>
<span>
{t("Total")} {t("Size")}:{" "}
<span className="font-medium text-foreground">{formatBytes(versionStats.totalSize)}</span>
</span>
</div>
<DataTable table={table} isLoading={loading} emptyTitle={t("No Versions")} />
</DialogContent>
</Dialog>
Expand Down
16 changes: 16 additions & 0 deletions hooks/use-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,21 @@ export function useObject(bucket: string) {
[client, bucket, deleteObject],
)

const restoreObjectVersion = useCallback(
async (key: string, versionId: string) => {
return client.send(
new CopyObjectCommand({
Bucket: bucket,
Key: key,
CopySource: encodeObjectCopySource(bucket, key, versionId),
MetadataDirective: "COPY",
TaggingDirective: "COPY",
}),
)
},
[client, bucket],
)

const listObject = useCallback(
async (
bucketName: string,
Expand Down Expand Up @@ -299,6 +314,7 @@ export function useObject(bucket: string) {
putObject,
deleteObject,
renameObject,
restoreObjectVersion,
getSignedUrl: getSignedUrlFn,
listObject,
mapAllFiles,
Expand Down
6 changes: 6 additions & 0 deletions i18n/locales/ar-MA.json
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@
"Current Site": "الموقع الحالي",
"Current User Policy": "سياسة المستخدم الحالية",
"Current Version": "الإصدار الحالي",
"Current": "Current",
"Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.": "حالة الخلفية الحالية للحاويات والمستخدمين والمجموعات والسياسات وقواعد انتهاء دورة الحياة المنسوخة.",
"Current site + remote peers": "الموقع الحالي + النظراء البعيدون",
"Current user policy": "سياسة المستخدم الحالية",
Expand Down Expand Up @@ -925,6 +926,10 @@
"Restart KMS": "إعادة تشغيل KMS",
"Restart Required": "إعادة التشغيل مطلوبة",
"Restore Key": "استعادة المفتاح",
"Restore": "Restore",
"Restore Failed": "Restore Failed",
"Restore Success": "Restore Success",
"Restore Version": "Restore Version",
"Resume": "استئناف",
"Resync cancel request sent successfully": "تم إرسال طلب إلغاء إعادة المزامنة بنجاح",
"Resync request started successfully": "تم بدء طلب إعادة المزامنة بنجاح",
Expand Down Expand Up @@ -1133,6 +1138,7 @@
"This action cannot be undone.": "لا يمكن التراجع عن هذا الإجراء.",
"This action retires the selected pool and should be used only after verifying rebalance has completed.": "يقوم هذا الإجراء بإخراج التجمع المحدد من الخدمة، ويجب استخدامه فقط بعد التأكد من اكتمال إعادة التوازن.",
"This key is used as the platform default SSE key for SSE-KMS and SSE-S3.": "يُستخدم هذا المفتاح كمفتاح SSE الافتراضي للمنصة لكل من SSE-KMS وSSE-S3.",
"This will copy the selected historical version and make it the current object version.": "This will copy the selected historical version and make it the current object version.",
"This key is used as the platform default for SSE-KMS.": "يُستخدم هذا المفتاح كافتراضي للمنصة لـ SSE-KMS.",
"This link will expire when your session ends or at the specified time.": "ستنتهي صلاحية هذا الرابط عند انتهاء جلستك أو في الوقت المحدد.",
"This permanently deletes the key immediately and cannot be undone.": "يحذف المفتاح نهائيًا فورًا ولا يمكن التراجع.",
Expand Down
6 changes: 6 additions & 0 deletions i18n/locales/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@
"Current Site": "Aktueller Standort",
"Current User Policy": "Aktuelle Benutzerrichtlinie",
"Current Version": "Aktuelle Version",
"Current": "Current",
"Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.": "Aktueller Backend-Status für replizierte Buckets, Benutzer, Gruppen, Richtlinien und Ablaufregeln des Lebenszyklus.",
"Current site + remote peers": "Aktuelle Site + entfernte Peers",
"Current user policy": "Aktuelle Benutzerrichtlinie",
Expand Down Expand Up @@ -925,6 +926,10 @@
"Restart KMS": "KMS neu starten",
"Restart Required": "Neustart erforderlich",
"Restore Key": "Schlüssel wiederherstellen",
"Restore": "Restore",
"Restore Failed": "Restore Failed",
"Restore Success": "Restore Success",
"Restore Version": "Restore Version",
"Resume": "Fortsetzen",
"Resync cancel request sent successfully": "Anfrage zum Abbrechen der Neusynchronisierung erfolgreich gesendet",
"Resync request started successfully": "Neusynchronisierungsanfrage erfolgreich gestartet",
Expand Down Expand Up @@ -1133,6 +1138,7 @@
"This action cannot be undone.": "Diese Aktion kann nicht rückgängig gemacht werden.",
"This action retires the selected pool and should be used only after verifying rebalance has completed.": "Diese Aktion legt den ausgewählten Pool still und sollte erst verwendet werden, nachdem der Neuausgleich abgeschlossen ist.",
"This key is used as the platform default SSE key for SSE-KMS and SSE-S3.": "Dieser Schlüssel wird als plattformweiter Standard-SSE-Schlüssel für SSE-KMS und SSE-S3 verwendet.",
"This will copy the selected historical version and make it the current object version.": "This will copy the selected historical version and make it the current object version.",
"This key is used as the platform default for SSE-KMS.": "Dieser Schlüssel ist die Plattform-Standardvorgabe für SSE-KMS.",
"This link will expire when your session ends or at the specified time.": "Dieser Link läuft ab, wenn Ihre Sitzung endet oder zum angegebenen Zeitpunkt.",
"This permanently deletes the key immediately and cannot be undone.": "Der Schlüssel wird sofort dauerhaft gelöscht und kann nicht rückgängig gemacht werden.",
Expand Down
6 changes: 6 additions & 0 deletions i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@
"Current Site": "Current Site",
"Current User Policy": "Current User Policy",
"Current Version": "Current Version",
"Current": "Current",
"Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.": "Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.",
"Current site + remote peers": "Current site + remote peers",
"Current user policy": "Current user policy",
Expand Down Expand Up @@ -925,6 +926,10 @@
"Restart KMS": "Restart KMS",
"Restart Required": "Restart Required",
"Restore Key": "Restore Key",
"Restore": "Restore",
"Restore Failed": "Restore Failed",
"Restore Success": "Restore Success",
"Restore Version": "Restore Version",
"Resume": "Resume",
"Resync cancel request sent successfully": "Resync cancel request sent successfully",
"Resync request started successfully": "Resync request started successfully",
Expand Down Expand Up @@ -1133,6 +1138,7 @@
"This action cannot be undone.": "This action cannot be undone.",
"This action retires the selected pool and should be used only after verifying rebalance has completed.": "This action retires the selected pool and should be used only after verifying rebalance has completed.",
"This key is used as the platform default SSE key for SSE-KMS and SSE-S3.": "This key is used as the platform default SSE key for SSE-KMS and SSE-S3.",
"This will copy the selected historical version and make it the current object version.": "This will copy the selected historical version and make it the current object version.",
"This key is used as the platform default for SSE-KMS.": "This key is used as the platform default for SSE-KMS.",
"This link will expire when your session ends or at the specified time.": "This link will expire when your session ends or at the specified time.",
"This permanently deletes the key immediately and cannot be undone.": "This permanently deletes the key immediately and cannot be undone.",
Expand Down
6 changes: 6 additions & 0 deletions i18n/locales/es-ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@
"Current Site": "Sitio Actual",
"Current User Policy": "Política de Usuario Actual",
"Current Version": "Versión Actual",
"Current": "Current",
"Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.": "Estado actual del backend para buckets, usuarios, grupos, políticas y reglas de expiración del ciclo de vida replicados.",
"Current site + remote peers": "Sitio actual + pares remotos",
"Current user policy": "Política de usuario actual",
Expand Down Expand Up @@ -925,6 +926,10 @@
"Restart KMS": "Reiniciar KMS",
"Restart Required": "Reinicio requerido",
"Restore Key": "Restaurar clave",
"Restore": "Restore",
"Restore Failed": "Restore Failed",
"Restore Success": "Restore Success",
"Restore Version": "Restore Version",
"Resume": "Reanudar",
"Resync cancel request sent successfully": "La solicitud para cancelar la resincronización se envió correctamente",
"Resync request started successfully": "La solicitud de resincronización se inició correctamente",
Expand Down Expand Up @@ -1133,6 +1138,7 @@
"This action cannot be undone.": "Esta acción no se puede deshacer.",
"This action retires the selected pool and should be used only after verifying rebalance has completed.": "Esta acción retira el pool seleccionado y solo debe usarse tras verificar que el reequilibrio ha finalizado.",
"This key is used as the platform default SSE key for SSE-KMS and SSE-S3.": "Esta clave se usa como la clave SSE predeterminada de la plataforma para SSE-KMS y SSE-S3.",
"This will copy the selected historical version and make it the current object version.": "This will copy the selected historical version and make it the current object version.",
"This key is used as the platform default for SSE-KMS.": "Esta clave se usa como predeterminada de la plataforma para SSE-KMS.",
"This link will expire when your session ends or at the specified time.": "Este enlace caducará cuando finalice su sesión o en la hora especificada.",
"This permanently deletes the key immediately and cannot be undone.": "Esto elimina la clave de forma permanente e inmediata y no se puede deshacer.",
Expand Down
6 changes: 6 additions & 0 deletions i18n/locales/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@
"Current Site": "Site actuel",
"Current User Policy": "Politique utilisateur actuelle",
"Current Version": "Version actuelle",
"Current": "Current",
"Current backend status for replicated buckets, users, groups, policies, and lifecycle expiry rules.": "État actuel du backend pour les buckets, utilisateurs, groupes, politiques et règles d’expiration du cycle de vie répliqués.",
"Current site + remote peers": "Site actuel + pairs distants",
"Current user policy": "Politique utilisateur actuelle",
Expand Down Expand Up @@ -925,6 +926,10 @@
"Restart KMS": "Redémarrer KMS",
"Restart Required": "Redémarrage requis",
"Restore Key": "Restaurer la clé",
"Restore": "Restore",
"Restore Failed": "Restore Failed",
"Restore Success": "Restore Success",
"Restore Version": "Restore Version",
"Resume": "Reprendre",
"Resync cancel request sent successfully": "La demande d'annulation de la resynchronisation a été envoyée avec succès",
"Resync request started successfully": "La demande de resynchronisation a été lancée avec succès",
Expand Down Expand Up @@ -1133,6 +1138,7 @@
"This action cannot be undone.": "Cette action ne peut pas être annulée.",
"This action retires the selected pool and should be used only after verifying rebalance has completed.": "Cette action retire le pool sélectionné et ne doit être utilisée qu’après avoir vérifié que le rééquilibrage est terminé.",
"This key is used as the platform default SSE key for SSE-KMS and SSE-S3.": "Cette clé est utilisée comme clé SSE par défaut de la plateforme pour SSE-KMS et SSE-S3.",
"This will copy the selected historical version and make it the current object version.": "This will copy the selected historical version and make it the current object version.",
"This key is used as the platform default for SSE-KMS.": "Cette clé est utilisée par défaut sur la plateforme pour SSE-KMS.",
"This link will expire when your session ends or at the specified time.": "Ce lien expirera lorsque votre session prendra fin ou à l'heure indiquée.",
"This permanently deletes the key immediately and cannot be undone.": "Supprime définitivement la clé immédiatement, sans retour en arrière.",
Expand Down
Loading