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
121 changes: 119 additions & 2 deletions components/object/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
RiArrowLeftSLine,
RiArrowRightSLine,
RiEyeLine,
RiEdit2Line,
} from "@remixicon/react"
import {
AlertDialog,
Expand All @@ -26,6 +27,15 @@ import {
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { SearchInput } from "@/components/search-input"
import { PageHeader } from "@/components/page-header"
Expand Down Expand Up @@ -63,6 +73,12 @@ import {
normalizeObjectZipDownloadUrl,
type CreateObjectZipDownloadResponse,
} from "@/lib/object-zip-download"
import {
buildRenamedObjectKey,
getObjectBaseName,
validateObjectRename,
type ObjectRenameValidation,
} from "@/lib/object-rename"
import { TaskStatsButton } from "@/components/tasks/stats-button"
import { useAddDeleteKeys, useAddDeleteFolder, useTasks } from "@/contexts/task-context"
import type { ColumnDef } from "@tanstack/react-table"
Expand Down Expand Up @@ -100,7 +116,7 @@ export function ObjectList({
const { t } = useTranslation()
const message = useMessage()
const api = useApi()
const { listObject, getSignedUrl } = useObject(bucket)
const { listObject, getSignedUrl, renameObject } = useObject(bucket)
const { getBucketVersioning } = useBucket()
const { canCapability } = usePermissions()
const addDeleteKeys = useAddDeleteKeys()
Expand All @@ -122,6 +138,10 @@ export function ObjectList({
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false)
const [deleteDialogKeys, setDeleteDialogKeys] = React.useState<string[]>([])
const [deleteAllVersions, setDeleteAllVersions] = React.useState(false)
const [renameDialogOpen, setRenameDialogOpen] = React.useState(false)
const [renameSourceKey, setRenameSourceKey] = React.useState("")
const [renameName, setRenameName] = React.useState("")
const [renameSubmitting, setRenameSubmitting] = React.useState(false)

const prefix = decodeURIComponent(path)
const resolvedPageSize = normalizeObjectListPageSize(storedPageSize)
Expand Down Expand Up @@ -333,6 +353,22 @@ export function ObjectList({
[message, t, getSignedUrl],
)

const getRenameValidationMessage = React.useCallback(
(validation: ObjectRenameValidation) => {
switch (validation) {
case "empty":
return t("Object name is required")
case "containsSlash":
return t("Object name cannot contain slashes")
case "sameName":
return t("New object name must be different")
}
},
[t],
)

const renameValidation = renameSourceKey ? validateObjectRename(renameSourceKey, renameName) : "empty"

const columns: ColumnDef<ObjectRow>[] = React.useMemo(
() => [
{
Expand Down Expand Up @@ -409,6 +445,12 @@ export function ObjectList({
<span>{t("Download")}</span>
</Button>
) : null}
{canCapability("objects.rename", { bucket, objectKey: row.original.Key, prefix }) ? (
<Button variant="outline" size="sm" onClick={() => openRenameDialog(row.original.Key)}>
<RiEdit2Line className="size-4" aria-hidden />
<span>{t("Rename")}</span>
</Button>
) : null}
</>
) : null}
{canCapability("objects.delete", { bucket, objectKey: row.original.Key }) ? (
Expand All @@ -421,7 +463,7 @@ export function ObjectList({
),
},
],
[t, displayKey, bucketPath, onOpenInfo, bucket, downloadFile, canCapability, onPreview],
[t, displayKey, bucketPath, onOpenInfo, bucket, downloadFile, canCapability, onPreview, prefix],
)

const { table, selectedRowIds } = useDataTable<ObjectRow>({
Expand Down Expand Up @@ -476,6 +518,50 @@ export function ObjectList({
setDeleteDialogOpen(true)
}

const openRenameDialog = (key: string) => {
setRenameSourceKey(key)
setRenameName(getObjectBaseName(key))
setRenameDialogOpen(true)
}

const handleRenameOpenChange = (open: boolean) => {
if (renameSubmitting) return
setRenameDialogOpen(open)
if (!open) {
setRenameSourceKey("")
setRenameName("")
}
}

const handleConfirmRename = async () => {
if (!renameSourceKey) return

const validation = validateObjectRename(renameSourceKey, renameName)
if (validation) {
message.warning(getRenameValidationMessage(validation))
return
}

const targetKey = buildRenamedObjectKey(renameSourceKey, renameName)
const loadingMsg = message.loading(t("Renaming object"), { duration: 0 })
setRenameSubmitting(true)

try {
await renameObject(renameSourceKey, targetKey)
message.success(t("Object renamed"))
table.resetRowSelection()
setRenameDialogOpen(false)
setRenameSourceKey("")
setRenameName("")
void fetchObjects({ resetPagination: true })
} catch (err) {
message.error((err as Error)?.message ?? t("Rename Failed"))
} finally {
loadingMsg.destroy()
setRenameSubmitting(false)
}
}

const handleConfirmDelete = async () => {
const keys = [...deleteDialogKeys]
setDeleteDialogOpen(false)
Expand Down Expand Up @@ -722,6 +808,37 @@ export function ObjectList({
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

<Dialog open={renameDialogOpen} onOpenChange={handleRenameOpenChange} disablePointerDismissal>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{t("Rename Object")}</DialogTitle>
<DialogDescription className="break-all">{renameSourceKey}</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<Input
name="object-rename-name"
value={renameName}
onChange={(e) => setRenameName(e.target.value)}
aria-label={t("Object Name")}
autoComplete="off"
spellCheck={false}
autoFocus
/>
{renameValidation ? (
<p className="text-xs text-destructive">{getRenameValidationMessage(renameValidation)}</p>
) : null}
</div>
<DialogFooter>
<Button variant="outline" disabled={renameSubmitting} onClick={() => handleRenameOpenChange(false)}>
{t("Cancel")}
</Button>
<Button disabled={Boolean(renameValidation) || renameSubmitting} onClick={handleConfirmRename}>
{t("Rename")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}
21 changes: 21 additions & 0 deletions hooks/use-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useCallback } from "react"
import {
CopyObjectCommand,
DeleteObjectCommand,
GetObjectLegalHoldCommand,
GetObjectRetentionCommand,
Expand All @@ -18,6 +19,7 @@ import {
} from "@aws-sdk/client-s3"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
import { useS3 } from "@/contexts/s3-context"
import { encodeObjectCopySource } from "@/lib/object-rename"
import { decodeS3UrlEncodedObjectList, decodeS3UrlEncodedObjectVersions } from "@/lib/s3-object-encoding"

function attachIncludeDeletedHeader(command: ListObjectsV2Command) {
Expand Down Expand Up @@ -88,6 +90,24 @@ export function useObject(bucket: string) {
[client, bucket],
)

const renameObject = useCallback(
async (sourceKey: string, targetKey: string) => {
await client.send(
new CopyObjectCommand({
Bucket: bucket,
Key: targetKey,
CopySource: encodeObjectCopySource(bucket, sourceKey),
IfNoneMatch: "*",
MetadataDirective: "COPY",
TaggingDirective: "COPY",
}),
)

return deleteObject(sourceKey)
},
[client, bucket, deleteObject],
)

const listObject = useCallback(
async (
bucketName: string,
Expand Down Expand Up @@ -278,6 +298,7 @@ export function useObject(bucket: string) {
headObject,
putObject,
deleteObject,
renameObject,
getSignedUrl: getSignedUrlFn,
listObject,
mapAllFiles,
Expand Down
10 changes: 9 additions & 1 deletion i18n/locales/ar-MA.json
Original file line number Diff line number Diff line change
Expand Up @@ -1346,5 +1346,13 @@
"Selection Locked": "Selection Locked",
"Decommission cleared": "Decommission cleared",
"Failed to clear decommission": "Failed to clear decommission",
"Clear Decommission": "Clear Decommission"
"Clear Decommission": "Clear Decommission",
"New object name must be different": "يجب أن يكون اسم الكائن الجديد مختلفًا",
"Object name cannot contain slashes": "لا يمكن أن يحتوي اسم الكائن على شرطات مائلة",
"Object name is required": "اسم الكائن مطلوب",
"Object renamed": "تمت إعادة تسمية الكائن",
"Rename": "إعادة التسمية",
"Rename Failed": "فشلت إعادة التسمية",
"Rename Object": "إعادة تسمية الكائن",
"Renaming object": "جارٍ إعادة تسمية الكائن"
}
10 changes: 9 additions & 1 deletion i18n/locales/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -1346,5 +1346,13 @@
"Selection Locked": "Selection Locked",
"Decommission cleared": "Decommission cleared",
"Failed to clear decommission": "Failed to clear decommission",
"Clear Decommission": "Clear Decommission"
"Clear Decommission": "Clear Decommission",
"New object name must be different": "Der neue Objektname muss anders sein",
"Object name cannot contain slashes": "Der Objektname darf keine Schrägstriche enthalten",
"Object name is required": "Der Objektname ist erforderlich",
"Object renamed": "Objekt wurde umbenannt",
"Rename": "Umbenennen",
"Rename Failed": "Umbenennen fehlgeschlagen",
"Rename Object": "Objekt umbenennen",
"Renaming object": "Objekt wird umbenannt"
}
10 changes: 9 additions & 1 deletion i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1346,5 +1346,13 @@
"Selection Locked": "Selection Locked",
"Decommission cleared": "Decommission cleared",
"Failed to clear decommission": "Failed to clear decommission",
"Clear Decommission": "Clear Decommission"
"Clear Decommission": "Clear Decommission",
"New object name must be different": "New object name must be different",
"Object name cannot contain slashes": "Object name cannot contain slashes",
"Object name is required": "Object name is required",
"Object renamed": "Object renamed",
"Rename": "Rename",
"Rename Failed": "Rename Failed",
"Rename Object": "Rename Object",
"Renaming object": "Renaming object"
}
10 changes: 9 additions & 1 deletion i18n/locales/es-ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -1346,5 +1346,13 @@
"Selection Locked": "Selection Locked",
"Decommission cleared": "Decommission cleared",
"Failed to clear decommission": "Failed to clear decommission",
"Clear Decommission": "Clear Decommission"
"Clear Decommission": "Clear Decommission",
"New object name must be different": "El nuevo nombre del objeto debe ser diferente",
"Object name cannot contain slashes": "El nombre del objeto no puede contener barras",
"Object name is required": "El nombre del objeto es obligatorio",
"Object renamed": "Objeto renombrado",
"Rename": "Renombrar",
"Rename Failed": "Error al renombrar",
"Rename Object": "Renombrar objeto",
"Renaming object": "Renombrando objeto"
}
10 changes: 9 additions & 1 deletion i18n/locales/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -1346,5 +1346,13 @@
"Selection Locked": "Selection Locked",
"Decommission cleared": "Decommission cleared",
"Failed to clear decommission": "Failed to clear decommission",
"Clear Decommission": "Clear Decommission"
"Clear Decommission": "Clear Decommission",
"New object name must be different": "Le nouveau nom de l’objet doit être différent",
"Object name cannot contain slashes": "Le nom de l’objet ne peut pas contenir de barres obliques",
"Object name is required": "Le nom de l’objet est obligatoire",
"Object renamed": "Objet renommé",
"Rename": "Renommer",
"Rename Failed": "Échec du renommage",
"Rename Object": "Renommer l’objet",
"Renaming object": "Renommage de l’objet"
}
10 changes: 9 additions & 1 deletion i18n/locales/id-ID.json
Original file line number Diff line number Diff line change
Expand Up @@ -1346,5 +1346,13 @@
"Selection Locked": "Selection Locked",
"Decommission cleared": "Decommission cleared",
"Failed to clear decommission": "Failed to clear decommission",
"Clear Decommission": "Clear Decommission"
"Clear Decommission": "Clear Decommission",
"New object name must be different": "Nama objek baru harus berbeda",
"Object name cannot contain slashes": "Nama objek tidak boleh berisi garis miring",
"Object name is required": "Nama objek wajib diisi",
"Object renamed": "Objek diganti namanya",
"Rename": "Ganti nama",
"Rename Failed": "Gagal mengganti nama",
"Rename Object": "Ganti nama objek",
"Renaming object": "Mengganti nama objek"
}
10 changes: 9 additions & 1 deletion i18n/locales/it-IT.json
Original file line number Diff line number Diff line change
Expand Up @@ -1346,5 +1346,13 @@
"Selection Locked": "Selection Locked",
"Decommission cleared": "Decommission cleared",
"Failed to clear decommission": "Failed to clear decommission",
"Clear Decommission": "Clear Decommission"
"Clear Decommission": "Clear Decommission",
"New object name must be different": "Il nuovo nome dell’oggetto deve essere diverso",
"Object name cannot contain slashes": "Il nome dell’oggetto non può contenere barre",
"Object name is required": "Il nome dell’oggetto è obbligatorio",
"Object renamed": "Oggetto rinominato",
"Rename": "Rinomina",
"Rename Failed": "Rinomina non riuscita",
"Rename Object": "Rinomina oggetto",
"Renaming object": "Rinomina oggetto in corso"
}
10 changes: 9 additions & 1 deletion i18n/locales/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -1346,5 +1346,13 @@
"Selection Locked": "Selection Locked",
"Decommission cleared": "Decommission cleared",
"Failed to clear decommission": "Failed to clear decommission",
"Clear Decommission": "Clear Decommission"
"Clear Decommission": "Clear Decommission",
"New object name must be different": "新しいオブジェクト名は異なる必要があります",
"Object name cannot contain slashes": "オブジェクト名にスラッシュは使用できません",
"Object name is required": "オブジェクト名は必須です",
"Object renamed": "オブジェクト名を変更しました",
"Rename": "名前を変更",
"Rename Failed": "名前の変更に失敗しました",
"Rename Object": "オブジェクト名を変更",
"Renaming object": "オブジェクト名を変更中"
}
10 changes: 9 additions & 1 deletion i18n/locales/ko-KR.json
Original file line number Diff line number Diff line change
Expand Up @@ -1346,5 +1346,13 @@
"Selection Locked": "Selection Locked",
"Decommission cleared": "Decommission cleared",
"Failed to clear decommission": "Failed to clear decommission",
"Clear Decommission": "Clear Decommission"
"Clear Decommission": "Clear Decommission",
"New object name must be different": "새 객체 이름은 달라야 합니다",
"Object name cannot contain slashes": "객체 이름에는 슬래시를 포함할 수 없습니다",
"Object name is required": "객체 이름은 필수입니다",
"Object renamed": "객체 이름이 변경되었습니다",
"Rename": "이름 변경",
"Rename Failed": "이름 변경 실패",
"Rename Object": "객체 이름 변경",
"Renaming object": "객체 이름 변경 중"
}
10 changes: 9 additions & 1 deletion i18n/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -1346,5 +1346,13 @@
"Selection Locked": "Selection Locked",
"Decommission cleared": "Decommission cleared",
"Failed to clear decommission": "Failed to clear decommission",
"Clear Decommission": "Clear Decommission"
"Clear Decommission": "Clear Decommission",
"New object name must be different": "O novo nome do objeto deve ser diferente",
"Object name cannot contain slashes": "O nome do objeto não pode conter barras",
"Object name is required": "O nome do objeto é obrigatório",
"Object renamed": "Objeto renomeado",
"Rename": "Renomear",
"Rename Failed": "Falha ao renomear",
"Rename Object": "Renomear objeto",
"Renaming object": "Renomeando objeto"
}
Loading