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 ? ( - )} - {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 && ( + + )} +
+ )} +
); } @@ -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} +

+
+ ) : ( + + )} +
+
+ ); +}