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 (
);
}
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.
);
}