import { useEffect, useState } from 'react'; import { useNavigate, useParams, Link } from 'react-router-dom'; import { objectsApi } from '@/lib/api'; import { useBuckets } from '@/hooks/useApi'; import { useBucketCan } from '@/hooks/usePermissions'; import type { ObjectMetadata } from '@/types'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { IconTile } from '@/components/ui/icon-tile'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { ArrowLeft, ChevronRight, Copy, Download, File, Loader2, Trash2 } from 'lucide-react'; import { toast } from 'sonner'; import { downloadObject, formatBytes } from '@/lib/file-utils'; import { formatDate } from '@/lib/utils'; function CardSection({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
); } function DetailRow({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
); } export function ObjectDetailsView() { const navigate = useNavigate(); const { bucketName, '*': encodedObjectKey } = useParams(); const objectKey = encodedObjectKey ? decodeURIComponent(encodedObjectKey) : undefined; const { data: buckets = [] } = useBuckets(); const bucket = buckets.find((b) => b.name === bucketName); const canBucket = useBucketCan(); const canDelete = canBucket(bucket, 'object.delete'); const [metadata, setMetadata] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [deleteOpen, setDeleteOpen] = useState(false); const [deleting, setDeleting] = useState(false); useEffect(() => { if (!bucketName || !objectKey) { setError('Bucket name and object key are required'); setIsLoading(false); return; } const fetchMetadata = async () => { try { setIsLoading(true); setError(null); const data = await objectsApi.getMetadata(bucketName, objectKey); setMetadata(data); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load object metadata'); } finally { setIsLoading(false); } }; fetchMetadata(); }, [bucketName, objectKey]); const parentPath = objectKey?.split('/').slice(0, -1).join('/') ?? ''; const fileName = objectKey?.split('/').pop() || objectKey || ''; const backHref = `/buckets/${bucketName}/objects${parentPath ? `?prefix=${encodeURIComponent(parentPath + '/')}` : ''}`; const pathSegments = parentPath ? parentPath.split('/').filter(Boolean) : []; const copy = (text: string, label = 'Copied') => { navigator.clipboard.writeText(text); toast.success(label); }; const handleDownload = () => { if (!bucketName || !objectKey) return; downloadObject(bucketName, objectKey); }; const handleDelete = async () => { if (!bucketName || !objectKey) return; try { setDeleting(true); await objectsApi.delete(bucketName, objectKey); toast.success('Object deleted'); navigate(backHref); } catch { // error toast handled by axios interceptor } finally { setDeleting(false); setDeleteOpen(false); } }; if (isLoading) { return (
Loading object details…
); } if (error || !metadata) { return (
{error || 'Object not found'}
); } return (
{/* Back + breadcrumb */}
Objects {pathSegments.map((seg, i) => ( {seg} ))} {fileName}
{/* Hero */}
} tone="primary" size="lg" />

{fileName}

{formatBytes(metadata.size)} {metadata.contentType || 'application/octet-stream'} {metadata.storageClass && {metadata.storageClass}}
{canDelete && ( )}
{/* Details */}
{formatBytes(metadata.size)} {metadata.contentType || 'application/octet-stream'} {metadata.storageClass || 'Standard'} {formatDate(metadata.lastModified)} {metadata.versionId && ( {metadata.versionId} )}
{/* Custom metadata */} {metadata.metadata && Object.keys(metadata.metadata).length > 0 && (
{Object.entries(metadata.metadata).map(([key, value]) => ( {value} ))}
)} {/* Preview */}
No preview available for this object.
); }