import { useEffect, useState } from 'react'; import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Skeleton } from '@/components/ui/skeleton'; import { Badge } from '@/components/ui/badge'; import { toast } from '@/components/ui/toast-store'; import { apiFetch } from '@/lib/api'; import { formatBytes } from '@/lib/utils'; import { copyToClipboard } from '@/lib/clipboard'; import { Image as ImageIcon, Copy } from 'lucide-react'; interface ImageInspect { Id: string; RepoTags?: string[] | null; RepoDigests?: string[] | null; Created: string; Size: number; Architecture?: string; Os?: string; Author?: string; Config?: { Cmd?: string[] | null; Entrypoint?: string[] | null; WorkingDir?: string; User?: string; Env?: string[] | null; Labels?: Record | null; ExposedPorts?: Record | null; }; RootFS?: { Type?: string; Layers?: string[] }; } interface ImageHistoryEntry { Id: string; Created: number; CreatedBy: string; Tags?: string[] | null; Size: number; Comment?: string; } interface ImageDetails { inspect: ImageInspect; history: ImageHistoryEntry[]; } interface ImageDetailsSheetProps { imageId: string | null; onClose: () => void; } function formatRelativeAge(timestampSec: number): string { const now = Date.now() / 1000; const diff = now - timestampSec; if (diff < 60) return 'just now'; if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; if (diff < 86400 * 30) return `${Math.floor(diff / 86400)}d ago`; if (diff < 86400 * 365) return `${Math.floor(diff / (86400 * 30))}mo ago`; return `${Math.floor(diff / (86400 * 365))}y ago`; } function shortDigest(id: string): string { const colon = id.indexOf(':'); const hex = colon >= 0 ? id.slice(colon + 1) : id; return hex.substring(0, 12); } export function ImageDetailsSheet({ imageId, onClose }: ImageDetailsSheetProps) { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); useEffect(() => { if (!imageId) { setData(null); return; } let cancelled = false; setLoading(true); setData(null); apiFetch(`/system/images/${encodeURIComponent(imageId)}`) .then(async (res) => { if (!res.ok) { throw new Error(res.status === 404 ? 'Image not found.' : 'Failed to load image details.'); } return res.json() as Promise; }) .then((details) => { if (!cancelled) setData(details); }) .catch((err: unknown) => { if (cancelled) return; const msg = err instanceof Error ? err.message : 'Failed to load image details.'; toast.error(msg); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [imageId]); const inspect = data?.inspect; const history = data?.history ?? []; const totalLayers = history.length; return ( !open && onClose()}> {inspect?.RepoTags?.[0] || (inspect ? shortDigest(inspect.Id) : 'Image details')} {loading && (
)} {!loading && inspect && (

{shortDigest(inspect.Id)}

{formatBytes(inspect.Size)}

{new Date(inspect.Created).toLocaleDateString()}

{inspect.Architecture ?? 'unknown'} / {inspect.Os ?? 'unknown'}

{inspect.Author && (

{inspect.Author}

)} {inspect.RepoTags && inspect.RepoTags.length > 0 && (
{inspect.RepoTags.map((t) => ( {t} ))}
)}
{inspect.Config && (
{inspect.Config.Env && inspect.Config.Env.length > 0 && ( )} {inspect.Config.Labels && Object.keys(inspect.Config.Labels).length > 0 && ( `${k}=${v}`)} /> )}
)}
{totalLayers === 0 ? (

No layer history available.

) : (
    {history.map((h, idx) => { const empty = h.Size === 0; return (
  1. #{totalLayers - idx} {formatBytes(h.Size)} {formatRelativeAge(h.Created)}

    {h.CreatedBy || '(no command)'}

    {h.Comment && (

    {h.Comment}

    )}
  2. ); })}
)}
)}
); } function Section({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
); } function Field({ label, children, span }: { label: string; children: React.ReactNode; span?: 1 | 2 }) { return (
{label} {children}
); } function ConfigRow({ label, value }: { label: string; value?: string }) { if (!value) return null; return (
{label} {value}
); } function CollapsibleList({ label, count, items }: { label: string; count: number; items: string[] }) { const [open, setOpen] = useState(false); return (
{label} ({count})
{open && (
    {items.map((item, i) => (
  1. {item}
  2. ))}
)}
); }