import { useState, useEffect, useRef } from "preact/hooks"; import { useOperations } from "../hooks/useOperations"; import type { Operation } from "../hooks/useOperations"; function formatElapsed(startedAt: number, completedAt: number | null): string { const end = completedAt || Date.now(); const seconds = Math.floor((end - startedAt) / 1000); if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); const secs = seconds % 60; return `${minutes}m ${secs}s`; } function formatBytes(bytes: number): string { if (bytes === 0) return "0 B"; const units = ["B", "KB", "MB", "GB"]; let i = 0; let size = bytes; while (size >= 1024 && i < units.length - 1) { size /= 1024; i++; } return `${size.toFixed(i === 0 ? 0 : 1)} ${units[i]}`; } function OpIcon({ type }: { type: string }) { switch (type) { case "upload": return ; case "clone": return ; case "delete": return ; default: return null; } } function StatusIndicator({ status }: { status: string }) { switch (status) { case "active": return ; case "done": return ( ); case "error": case "cancelled": return ( ); default: return null; } } function OperationRow({ op, onCancel }: { op: Operation; onCancel: (id: string) => void }) { const [elapsed, setElapsed] = useState(formatElapsed(op.startedAt, op.completedAt)); useEffect(() => { if (op.status !== "active") { setElapsed(formatElapsed(op.startedAt, op.completedAt)); return; } const interval = setInterval(() => { setElapsed(formatElapsed(op.startedAt, null)); }, 1000); return () => clearInterval(interval); }, [op.status, op.startedAt, op.completedAt]); const hasProgress = op.status === "active" && op.progress > 0; const isIndeterminate = op.status === "active" && op.progress === 0; const hasBytes = "totalBytes" in op && (op as any).totalBytes > 0 && op.status === "active"; return (
{op.label} {hasProgress && ( {Math.round(op.progress)}% )} {isIndeterminate && ( )} {hasBytes && ( {formatBytes((op as any).processedBytes)}/{formatBytes((op as any).totalBytes)} )} {elapsed} {op.status === "active" && ( )} {op.error && op.status !== "active" && ( {op.error} )}
); } export function OperationsPanel() { const { operations, cancelOperation, clearCompleted } = useOperations(); const [expanded, setExpanded] = useState(false); const panelRef = useRef(null); const activeOps = operations.filter((op) => op.status === "active"); const completedOps = operations.filter((op) => op.status !== "active"); const hasOps = operations.length > 0; // Communicate panel height to CSS via custom property on useEffect(() => { const update = () => { const h = panelRef.current ? panelRef.current.offsetHeight : 0; document.documentElement.style.setProperty("--ops-panel-h", `${h}px`); }; update(); // Re-measure after expand/collapse transitions const timer = setTimeout(update, 160); return () => clearTimeout(timer); }, [hasOps, expanded, operations.length]); // Clean up on unmount useEffect(() => { return () => { document.documentElement.style.setProperty("--ops-panel-h", "0px"); }; }, []); if (!hasOps) { // Ensure CSS var is reset document.documentElement.style.setProperty("--ops-panel-h", "0px"); return null; } return (
setExpanded(!expanded)}> {activeOps.length > 0 ? `${activeOps.length} operation${activeOps.length > 1 ? "s" : ""} in progress` : `${completedOps.length} operation${completedOps.length > 1 ? "s" : ""} completed` } {completedOps.length > 0 && activeOps.length === 0 && ( )}
{expanded && (
{operations.map((op) => ( ))}
)}
); }