mirror of
https://github.com/Noooste/garage-ui.git
synced 2026-08-20 09:42:17 +00:00
bfed48421b
Signed-off-by: Noooste <83548733+Noooste@users.noreply.github.com>
106 lines
3.6 KiB
TypeScript
106 lines
3.6 KiB
TypeScript
import { NavLink, Outlet, useParams } from 'react-router-dom';
|
|
import { Database, Copy, Upload } from 'lucide-react';
|
|
import { IconTile } from '@/components/ui/icon-tile';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { cn } from '@/lib/utils';
|
|
import { useBuckets } from '@/hooks/useApi';
|
|
import { toast } from 'sonner';
|
|
|
|
interface TabSpec {
|
|
to: string;
|
|
label: string;
|
|
end?: boolean;
|
|
}
|
|
|
|
const tabs: TabSpec[] = [
|
|
{ to: 'objects', label: 'Objects' },
|
|
{ to: 'permissions', label: 'Permissions' },
|
|
{ to: 'website', label: 'Website' },
|
|
{ to: 'settings', label: 'Settings' },
|
|
];
|
|
|
|
function formatBytes(n?: number) {
|
|
if (n == null) return '';
|
|
if (n < 1024) return `${n} B`;
|
|
const units = ['KB', 'MB', 'GB', 'TB'];
|
|
let v = n / 1024;
|
|
for (const u of units) {
|
|
if (v < 1024) return `${v.toFixed(v >= 10 ? 0 : 1)} ${u}`;
|
|
v /= 1024;
|
|
}
|
|
return `${v.toFixed(0)} PB`;
|
|
}
|
|
|
|
export function BucketDetailShell() {
|
|
const { bucketName = '' } = useParams<{ bucketName: string }>();
|
|
const { data: buckets = [] } = useBuckets();
|
|
const bucket = buckets.find((b) => b.name === bucketName);
|
|
|
|
const s3Url = `s3://${bucketName}`;
|
|
const copyUrl = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(s3Url);
|
|
toast.success('URL copied');
|
|
} catch {
|
|
toast.error('Failed to copy');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col">
|
|
{/* Hero */}
|
|
<section className="px-7 pt-6 pb-5">
|
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
<div className="flex min-w-0 items-start gap-3">
|
|
<IconTile icon={<Database />} tone="primary" size="lg" />
|
|
<div className="min-w-0">
|
|
<h1 className="truncate text-[26px] font-semibold tracking-[-0.02em]">{bucketName}</h1>
|
|
<p className="mt-1 truncate font-mono text-[13.5px] text-[var(--muted-foreground)]">{s3Url}</p>
|
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
<Badge variant="success">Active</Badge>
|
|
{bucket?.objectCount != null && <Badge>{bucket.objectCount.toLocaleString()} objects</Badge>}
|
|
{bucket?.size != null && <Badge>{formatBytes(bucket.size)}</Badge>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex shrink-0 items-center gap-2">
|
|
<Button variant="secondary" onClick={copyUrl}>
|
|
<Copy /> Copy URL
|
|
</Button>
|
|
<Button variant="primary" onClick={() => document.dispatchEvent(new CustomEvent('bucket:upload'))}>
|
|
<Upload /> Upload
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Tabs */}
|
|
<nav className="flex h-12 items-center gap-0 border-b border-[var(--border)] px-7">
|
|
{tabs.map((t) => (
|
|
<NavLink
|
|
key={t.to}
|
|
to={t.to}
|
|
end={t.end}
|
|
className={({ isActive }) =>
|
|
cn(
|
|
'relative -mb-px inline-flex h-12 items-center px-3.5 text-[14px] font-medium transition-colors',
|
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] rounded-sm',
|
|
isActive
|
|
? 'text-[var(--primary)] border-b-2 border-[var(--primary)]'
|
|
: 'text-[var(--muted-foreground)] border-b-2 border-transparent hover:text-[var(--foreground)]',
|
|
)
|
|
}
|
|
>
|
|
{t.label}
|
|
</NavLink>
|
|
))}
|
|
</nav>
|
|
|
|
<div className="min-w-0">
|
|
<Outlet />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|