Skip to content
Closed
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
26 changes: 0 additions & 26 deletions app/(dashboard)/browser/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { ObjectList } from "@/components/object/list"
import { ObjectView } from "@/components/object/view"
import { ObjectInfo } from "@/components/object/info"
import { ObjectUploadPicker } from "@/components/object/upload-picker"
import { useBucket } from "@/hooks/use-bucket"
import { useMessage } from "@/lib/feedback/message"
import { buildBucketPath } from "@/lib/bucket-path"
import { useTasks } from "@/contexts/task-context"
Expand All @@ -32,7 +31,6 @@ export function BrowserContent({ bucketName, keyPath = "", preview = false, prev
const searchParams = useSearchParams()
const message = useMessage()
const { canCapability } = usePermissions()
const { headBucket } = useBucket()

const isObjectList = keyPath.endsWith("/") || keyPath === ""
const prefix = keyPath.endsWith("/") ? keyPath : keyPath ? `${keyPath}/` : ""
Expand All @@ -46,30 +44,6 @@ export function BrowserContent({ bucketName, keyPath = "", preview = false, prev
const objectApi = useObject(bucketName)
const canUploadObjects = canCapability("objects.upload", { bucket: bucketName, prefix })

React.useEffect(() => {
if (!bucketName) return
headBucket(bucketName)
.then(() => {})
.catch((error: unknown) => {
const err = error as { $metadata?: { httpStatusCode?: number }; Code?: string; message?: string }
const status = err?.$metadata?.httpStatusCode
const code = (err?.Code ?? (error as Error)?.message ?? "").toLowerCase()
const isAccessDenied =
status === 403 ||
code === "accessdenied" ||
code === "forbidden" ||
(typeof code === "string" && (code.includes("access denied") || code.includes("forbidden")))
message.error(isAccessDenied ? t("Access Denied") : t("Bucket not found"))
const params = new URLSearchParams(searchParams.toString())
params.delete("bucket")
params.delete("prefix")
params.delete("preview")
params.delete("previewKey")
const query = params.toString()
router.push(query ? `/browser?${query}` : "/browser")
})
}, [bucketName, headBucket, message, router, t, searchParams])

const bucketPath = React.useCallback((path?: string | string[]) => buildBucketPath(bucketName, path), [bucketName])

const handlePathClick = (path: string) => {
Expand Down
24 changes: 18 additions & 6 deletions app/(dashboard)/sse/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -545,9 +545,7 @@ export default function SSEPage() {
if (values.backendType === "static") {
if (!values.secretKey.trim()) {
return {
error: t(
"Please enter the static KMS secret key (base64-encoded 32-byte AES-256 key).",
),
error: t("Please enter the static KMS secret key (base64-encoded 32-byte AES-256 key)."),
field: "secretKey",
}
}
Expand Down Expand Up @@ -1548,7 +1546,13 @@ export default function SSEPage() {
<Button
size="sm"
onClick={() => setCreateKeyOpen(true)}
disabled={Boolean(activeMutation) || loadingKeys || loadingStatus || Boolean(keysError) || staticKmsReadOnly}
disabled={
Boolean(activeMutation) ||
loadingKeys ||
loadingStatus ||
Boolean(keysError) ||
staticKmsReadOnly
}
>
<RiAddLine className="size-4" aria-hidden />
{t("Create Key")}
Expand Down Expand Up @@ -1642,7 +1646,11 @@ export default function SSEPage() {
variant="outline"
className="min-h-11 flex-1 sm:flex-none"
disabled={
isDefaultKey || Boolean(activeMutation) || loadingStatus || Boolean(keysError) || staticKmsReadOnly
isDefaultKey ||
Boolean(activeMutation) ||
loadingStatus ||
Boolean(keysError) ||
staticKmsReadOnly
}
onClick={() => setPendingKeyAction({ type: "scheduleDelete", key })}
>
Expand All @@ -1654,7 +1662,11 @@ export default function SSEPage() {
variant="destructive"
className="min-h-11 flex-1 sm:flex-none"
disabled={
isDefaultKey || Boolean(activeMutation) || loadingStatus || Boolean(keysError) || staticKmsReadOnly
isDefaultKey ||
Boolean(activeMutation) ||
loadingStatus ||
Boolean(keysError) ||
staticKmsReadOnly
}
onClick={() => setPendingKeyAction({ type: "forceDelete", key })}
>
Expand Down
6 changes: 5 additions & 1 deletion components/data-table/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface DataTableProps<TData> {
isLoading?: boolean
emptyTitle?: string
emptyDescription?: string
emptyAction?: React.ReactNode
caption?: string
className?: string
tableClass?: string
Expand Down Expand Up @@ -69,6 +70,7 @@ export function DataTable<TData>({
isLoading = false,
emptyTitle = "No data",
emptyDescription = "There is nothing to display yet.",
emptyAction,
caption,
className,
tableClass,
Expand Down Expand Up @@ -153,7 +155,9 @@ export function DataTable<TData>({
) : (
<TableRow>
<TableCell colSpan={visibleColumnCount} className="h-48">
<EmptyState title={emptyTitle} description={emptyDescription} />
<EmptyState title={emptyTitle} description={emptyDescription}>
{emptyAction}
</EmptyState>
</TableCell>
</TableRow>
)}
Expand Down
94 changes: 69 additions & 25 deletions components/object/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { useLocalStorage } from "@/hooks/use-local-storage"
import { usePermissions } from "@/hooks/use-permissions"
import { useApi } from "@/contexts/api-context"
import { useMessage } from "@/lib/feedback/message"
import { isAccessDeniedError } from "@/lib/error-handler"
import { exportFile } from "@/lib/export-file"
import { getContentType } from "@/lib/mime-types"
import { formatBytes, formatDateTime } from "@/lib/functions"
Expand All @@ -57,6 +58,7 @@ import {
resolveObjectListDisplayState,
shouldApplyObjectListResponse,
shouldResetObjectListPagination,
type ObjectListErrorState,
} from "@/lib/object-list-state"
import { OBJECT_LIST_DEFAULT_PAGE_SIZE, resolveObjectListPageSize } from "@/lib/object-list-pagination"
import {
Expand Down Expand Up @@ -115,7 +117,7 @@ export function ObjectList({
const api = useApi()
const { listObject, getSignedUrl, renameObject } = useObject(bucket)
const { getBucketVersioning } = useBucket()
const { canCapability } = usePermissions()
const { canCapability, hasPermission } = usePermissions()
const addDeleteKeys = useAddDeleteKeys()
const addDeleteFolder = useAddDeleteFolder()
const tasks = useTasks()
Expand All @@ -129,6 +131,7 @@ export function ObjectList({
const [bucketVersioningState, setBucketVersioningState] = React.useState<BucketVersioningState>("unknown")
const [versioningError, setVersioningError] = React.useState("")
const [versioningReload, setVersioningReload] = React.useState(0)
const [listError, setListError] = React.useState<ObjectListErrorState>(null)
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false)
const [deleteDialogKeys, setDeleteDialogKeys] = React.useState<string[]>([])
const [deleteAllVersions, setDeleteAllVersions] = React.useState(false)
Expand All @@ -151,6 +154,7 @@ export function ObjectList({
)
const canBulkDelete = canCapability("objects.bulkDelete", { bucket, prefix })
const canBulkDownload = canCapability("objects.download", { bucket, prefix })
const shouldLoadBucketVersioning = hasPermission("s3:DeleteObject")

const bucketPath = React.useCallback((p?: string | string[]) => buildBucketPath(bucket, p), [bucket])
const requestIdRef = React.useRef(0)
Expand Down Expand Up @@ -203,6 +207,9 @@ export function ObjectList({
const requestScope = activeScopeRef.current
loadingRef.current = true
setLoading(true)
if (!shouldAppend) {
setListError(null)
}
try {
const response = await listObject(bucket, prefix || undefined, resolvedPageSize, token, {
includeDeleted: showDeleted,
Expand Down Expand Up @@ -249,7 +256,6 @@ export function ObjectList({
}
} catch (error) {
console.error("Failed to fetch objects:", error)
message.error((error as Error)?.message ?? t("Failed to load objects"))
if (
shouldApplyObjectListResponse({
requestId,
Expand All @@ -258,9 +264,15 @@ export function ObjectList({
activeScope: activeScopeRef.current,
})
) {
const accessDenied = isAccessDeniedError(error)
message.error(accessDenied ? t("Access Denied") : ((error as Error)?.message ?? t("Failed to load objects")))
setNextToken(undefined)
if (!shouldAppend) {
if (accessDenied) {
setData([])
setListError("access-denied")
} else if (!shouldAppend) {
setData([])
setListError("error")
}
}
} finally {
Expand Down Expand Up @@ -323,6 +335,12 @@ export function ObjectList({
}, [tasks, resetAndFetchObjects])

React.useEffect(() => {
if (!shouldLoadBucketVersioning) {
setBucketVersioningState("unknown")
setVersioningError("")
return
}

let cancelled = false

const loadBucketVersioningStatus = async () => {
Expand All @@ -337,8 +355,13 @@ export function ObjectList({
} catch (error) {
console.error("Failed to load bucket versioning status:", error)
if (!cancelled) {
setBucketVersioningState("unknown")
setVersioningError(t("Failed to get data"))
if (isAccessDeniedError(error)) {
setBucketVersioningState("unknown")
setVersioningError(t("Unable to load versioning status."))
} else {
setBucketVersioningState("unknown")
setVersioningError(t("Failed to get data"))
}
}
}
}
Expand All @@ -348,7 +371,7 @@ export function ObjectList({
return () => {
cancelled = true
}
}, [bucket, getBucketVersioning, t, versioningReload])
}, [bucket, getBucketVersioning, shouldLoadBucketVersioning, t, versioningReload])

const displayKey = React.useCallback(
(key: string) => {
Expand All @@ -374,18 +397,29 @@ export function ObjectList({
loadedCount: data.length,
hasMore: Boolean(nextToken),
loading,
error: listError,
})
const filteredEmptyState = displayState === "filtered-partial" || displayState === "filtered-empty"
const emptyTitle = filteredEmptyState
? t(displayState === "filtered-partial" ? "No matches in loaded objects" : "No matching objects")
: t("No Objects")
const emptyDescription = filteredEmptyState
? t(
displayState === "filtered-partial"
? "More objects have not been searched yet."
: "No loaded objects match this filter.",
)
: t("Upload files or create folders to populate this bucket.")
const emptyTitle =
displayState === "access-denied"
? t("Access Denied")
: displayState === "error"
? t("Failed to load objects")
: filteredEmptyState
? t(displayState === "filtered-partial" ? "No matches in loaded objects" : "No matching objects")
: t("No Objects")
const emptyDescription =
displayState === "access-denied"
? t("Ask your administrator to grant permission to list objects in this bucket.")
: displayState === "error"
? t("Refresh to try again.")
: filteredEmptyState
? t(
displayState === "filtered-partial"
? "More objects have not been searched yet."
: "No loaded objects match this filter.",
)
: t("Upload files or create folders to populate this bucket.")

const downloadFile = React.useCallback(
async (key: string) => {
Expand Down Expand Up @@ -824,7 +858,7 @@ export function ObjectList({
</div>
</PageHeader>

{bucketVersioningState === "unknown" ? (
{shouldLoadBucketVersioning && !listError && bucketVersioningState === "unknown" ? (
<div
id="object-versioning-status"
role={versioningError ? "alert" : "status"}
Expand Down Expand Up @@ -852,16 +886,26 @@ export function ObjectList({
isLoading={displayState === "loading" || displayState === "filtered-loading"}
emptyTitle={emptyTitle}
emptyDescription={emptyDescription}
emptyAction={
displayState === "error" ? (
<Button type="button" variant="outline" onClick={resetAndFetchObjects}>
<RiRefreshLine className="size-4" aria-hidden />
{t("Refresh")}
</Button>
) : undefined
}
/>

<div className="flex flex-wrap items-center justify-between gap-3 text-sm text-muted-foreground">
<span>
{t("Loaded {count} objects", {
count: data.length,
})}
</span>
<span>{t("Filtering and sorting apply to loaded objects")}</span>
</div>
{!listError ? (
<div className="flex flex-wrap items-center justify-between gap-3 text-sm text-muted-foreground">
<span>
{t("Loaded {count} objects", {
count: data.length,
})}
</span>
<span>{t("Filtering and sorting apply to loaded objects")}</span>
</div>
) : null}

{nextToken ? (
<div ref={loadMoreRef} className="flex min-h-10 items-center justify-center text-sm text-muted-foreground">
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/ar-MA.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"AWS S3": "AWS S3",
"Access Control": "التحكم في الوصول",
"Access Denied": "تم رفض الوصول",
"Ask your administrator to grant permission to list objects in this bucket.": "اطلب من المسؤول منحك إذن عرض الكائنات في هذه الحاوية.",
"Access Key": "مفتاح الوصول",
"Access Key *": "مفتاح الوصول *",
"Access Key is required": "مفتاح الوصول مطلوب",
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"AWS S3": "AWS S3",
"Access Control": "Zugriffskontrolle",
"Access Denied": "Zugriff verweigert",
"Ask your administrator to grant permission to list objects in this bucket.": "Bitten Sie Ihren Administrator, die Berechtigung zum Auflisten der Objekte in diesem Bucket zu erteilen.",
"Access Key": "Zugriffsschlüssel",
"Access Key *": "Zugriffsschlüssel *",
"Access Key is required": "Zugriffsschlüssel ist erforderlich",
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"AWS S3": "AWS S3",
"Access Control": "Access Control",
"Access Denied": "Access Denied",
"Ask your administrator to grant permission to list objects in this bucket.": "Ask your administrator to grant permission to list objects in this bucket.",
"Access Key": "Access Key",
"Access Key *": "Access Key *",
"Access Key is required": "Access Key is required",
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/es-ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"AWS S3": "AWS S3",
"Access Control": "Control de Acceso",
"Access Denied": "Acceso Denegado",
"Ask your administrator to grant permission to list objects in this bucket.": "Pida a su administrador que conceda permiso para enumerar los objetos de este bucket.",
"Access Key": "Clave de Acceso",
"Access Key *": "Clave de Acceso *",
"Access Key is required": "La clave de acceso es obligatoria",
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"AWS S3": "AWS S3",
"Access Control": "Contrôle d'accès",
"Access Denied": "Accès Refusé",
"Ask your administrator to grant permission to list objects in this bucket.": "Demandez à votre administrateur d’accorder l’autorisation de répertorier les objets de ce bucket.",
"Access Key": "Clé d'accès",
"Access Key *": "Clé d'accès *",
"Access Key is required": "La clé d'accès est requise",
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/id-ID.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"AWS S3": "AWS S3",
"Access Control": "Kontrol Akses",
"Access Denied": "Akses Ditolak",
"Ask your administrator to grant permission to list objects in this bucket.": "Minta administrator Anda memberikan izin untuk mencantumkan objek dalam bucket ini.",
"Access Key": "Access Key",
"Access Key *": "Access Key *",
"Access Key is required": "Access Key wajib diisi",
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/it-IT.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"AWS S3": "AWS S3",
"Access Control": "Controllo accessi",
"Access Denied": "Accesso Negato",
"Ask your administrator to grant permission to list objects in this bucket.": "Chiedi all’amministratore di concedere l’autorizzazione per elencare gli oggetti in questo bucket.",
"Access Key": "Chiave di accesso",
"Access Key *": "Chiave di accesso *",
"Access Key is required": "La chiave di accesso è obbligatoria",
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"AWS S3": "AWS S3",
"Access Control": "アクセス制御",
"Access Denied": "アクセス拒否",
"Ask your administrator to grant permission to list objects in this bucket.": "このバケット内のオブジェクトを一覧表示する権限を管理者に付与してもらってください。",
"Access Key": "アクセスキー",
"Access Key *": "アクセスキー *",
"Access Key is required": "アクセスキーは必須です",
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/ko-KR.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"AWS S3": "AWS S3",
"Access Control": "액세스 제어",
"Access Denied": "액세스 거부",
"Ask your administrator to grant permission to list objects in this bucket.": "관리자에게 이 버킷의 객체를 나열할 수 있는 권한을 요청하세요.",
"Access Key": "액세스 키",
"Access Key *": "액세스 키 *",
"Access Key is required": "액세스 키가 필요합니다",
Expand Down
Loading