diff --git a/apps/web/app/components/charts/report-card.tsx b/apps/web/app/components/charts/report-card.tsx index 670764e040d..12e4d7a33a0 100644 --- a/apps/web/app/components/charts/report-card.tsx +++ b/apps/web/app/components/charts/report-card.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState, useCallback } from "react"; +import { motion, AnimatePresence } from "framer-motion"; import { ChartPanel } from "./chart-panel"; import type { ReportConfig, PanelConfig } from "./types"; @@ -20,12 +21,24 @@ function ChartBarIcon() { ); } -function ExternalLinkIcon() { +function ExpandIcon() { return ( - - + + + + + ); +} + +function CollapseIcon() { + return ( + + + + + ); } @@ -39,6 +52,17 @@ function PinIcon() { ); } +function RefreshIcon() { + return ( + + + + + + + ); +} + // --- Panel data state --- type PanelData = { @@ -47,26 +71,42 @@ type PanelData = { error?: string; }; +// --- Grid size helpers --- + +function panelColSpan(size?: string): string { + switch (size) { + case "full": + return "col-span-6"; + case "third": + return "col-span-2"; + case "half": + default: + return "col-span-3"; + } +} + // --- Main ReportCard --- export function ReportCard({ config }: ReportCardProps) { const [panelData, setPanelData] = useState>({}); const [pinning, setPinning] = useState(false); const [pinned, setPinned] = useState(false); + const [expanded, setExpanded] = useState(false); - // Show at most 2 panels inline - const visiblePanels = config.panels.slice(0, 2); + // In compact mode show at most 2 panels; expanded shows all + const visiblePanels = expanded ? config.panels : config.panels.slice(0, 2); + const hasMore = config.panels.length > 2; // Execute panel SQL queries - const executePanels = useCallback(async () => { + const executePanels = useCallback(async (panels: PanelConfig[]) => { const initial: Record = {}; - for (const panel of visiblePanels) { + for (const panel of panels) { initial[panel.id] = { rows: [], loading: true }; } - setPanelData(initial); + setPanelData((prev) => ({ ...prev, ...initial })); await Promise.all( - visiblePanels.map(async (panel) => { + panels.map(async (panel) => { try { const res = await fetch("/api/workspace/reports/execute", { method: "POST", @@ -94,14 +134,34 @@ export function ReportCard({ config }: ReportCardProps) { } }), ); - }, [visiblePanels]); + }, []); + // Load initial compact panels useEffect(() => { - void executePanels(); + void executePanels(config.panels.slice(0, 2)); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // Pin report to workspace filesystem + // When expanding, fetch any panels not yet loaded + const handleToggleExpand = useCallback(() => { + setExpanded((prev) => { + const next = !prev; + if (next && hasMore) { + const unloaded = config.panels.filter((p) => !panelData[p.id]); + if (unloaded.length > 0) { + void executePanels(unloaded); + } + } + return next; + }); + }, [hasMore, config.panels, panelData, executePanels]); + + // Refresh all visible panels + const handleRefresh = useCallback(() => { + void executePanels(expanded ? config.panels : config.panels.slice(0, 2)); + }, [expanded, config.panels, executePanels]); + + // Pin report to workspace /reports directory const handlePin = async () => { setPinning(true); try { @@ -130,7 +190,7 @@ export function ReportCard({ config }: ReportCardProps) { return ( - {!pinned && ( + {expanded && ( + + + + )} + {!pinned ? ( {pinning ? "Saving..." : "Pin"} - )} - {pinned && ( + ) : ( - Saved + + + + Pinned )} - - - Open - + {expanded ? : } + {expanded ? "Collapse" : "Open"} + @@ -212,28 +287,62 @@ export function ReportCard({ config }: ReportCardProps) { )} - {/* Panels (compact mode) */} - 1 ? "grid-cols-2" : "grid-cols-1"}`}> - {visiblePanels.map((panel) => ( - - ))} - + + {expanded ? ( + /* ── Expanded: full grid with all panels ── */ + + + {config.panels.map((panel) => ( + + ))} + + + ) : ( + /* ── Compact: max 2 panels ── */ + + 1 ? "grid-cols-2" : "grid-cols-1"}`}> + {visiblePanels.map((panel) => ( + + ))} + - {/* More panels indicator */} - {config.panels.length > 2 && ( - - - +{config.panels.length - 2} more chart{config.panels.length - 2 !== 1 ? "s" : ""} - - - )} + {/* More panels indicator */} + {hasMore && ( + + + +{config.panels.length - 2} more chart{config.panels.length - 2 !== 1 ? "s" : ""} — click to expand + + + )} + + )} + ); } @@ -287,3 +396,69 @@ function CompactPanelCard({ ); } + +// --- Expanded panel card for full report view --- + +function ExpandedPanelCard({ + panel, + data, +}: { + panel: PanelConfig; + data?: PanelData; +}) { + const colSpan = panelColSpan(panel.size); + + return ( + + + + {panel.title} + + {data && !data.loading && !data.error && ( + + {data.rows.length} rows + + )} + + + {data?.loading ? ( + + + + ) : data?.error ? ( + + + Query error + + + {data.error} + + + ) : ( + + )} + + + ); +}
+ Query error +
+ {data.error} +