Detail on demand A small panel becomes a big one for a moment. The expanded view must keep the same time range and the same series, or the reader compares two different things and does not notice. npx shadcn@latest add button toggle-group npm i lucide-react Tokens this needs: --background, --card, --card-foreground, --muted-foreground, --border, --chart-1 ──────────────────────────────────────────────────────────────────────── // DetailOnDemand.tsx ──────────────────────────────────────────────────────────────────────── import { useEffect, useRef } from "react"; import { Maximize2, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; export type Form = "full-page" | "side-panel" | "inline"; export type Panel = { id: string; title: string; unit: string; values: number[] }; /** The one query every panel shares. Both sizes render from it, so * expanding cannot change the interval or the range. */ export type Range = { label: string; interval: string }; const FORM_LABEL: Record = { "full-page": "Full page", "side-panel": "Side panel", inline: "Inline" }; /** One path, scaled to whatever box it is in. */ function Line({ values, className }: { values: number[]; className?: string }) { const min = Math.min(...values), max = Math.max(...values), span = max - min || 1; const d = values.map((v, i) => `${i ? "L" : "M"}${(i / (values.length - 1)) * 100} ${100 - ((v - min) / span) * 100}`).join(" "); return ( ); } function PanelCard({ panel, range, large, onToggle, buttonRef }: { panel: Panel; range: Range; large: boolean; onToggle: () => void; buttonRef?: (el: HTMLButtonElement | null) => void; }) { const last = panel.values[panel.values.length - 1]; return (

{panel.title}

{last.toLocaleString("en-GB")}{panel.unit}

{/* The same button opens and closes; Esc closes too. */}
{/* The range is printed at both sizes, from the same object, so the two views cannot disagree. */}

{range.label} · {range.interval}{large && ` · ${panel.values.length} points`}

); } export function DetailOnDemand({ panels, range, form, expanded, onExpand, columns = 3 }: { panels: Panel[]; range: Range; form: Form; /** Which panel is open, or null. Lift it into the URL so the page a * colleague opens is the one you found. */ expanded: string | null; onExpand: (id: string | null) => void; columns?: number; }) { const buttons = useRef(new Map()); const open = panels.find((p) => p.id === expanded) ?? null; useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => e.key === "Escape" && onExpand(null); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [open, onExpand]); // Closing puts focus back on the button that opened it, so keyboard users // land where they were rather than at the top of the page. const close = (id: string) => { onExpand(null); requestAnimationFrame(() => buttons.current.get(id)?.focus()); }; const grid = (
{panels.map((p) => { const large = form === "inline" && p.id === expanded; return (
(p.id === expanded ? close(p.id) : onExpand(p.id))} buttonRef={(el) => el && buttons.current.set(p.id, el)} />
); })}
); return (
{form === "side-panel" && open ? (
{grid} close(open.id)} />
) : grid} {/* In a page this is `fixed inset-0`; the preview keeps it inside the frame. */} {form === "full-page" && open && (
close(open.id)} />
)}
); } export const FORMS = Object.entries(FORM_LABEL) as [Form, string][]; ──────────────────────────────────────────────────────────────────────── // demo.tsx ──────────────────────────────────────────────────────────────────────── import { useState } from "react"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { DetailOnDemand, FORMS, type Form, type Panel } from "./DetailOnDemand"; /** Six panels for one service over the last six hours at five-minute * intervals. p95 latency is the one that moved. */ const PANELS: Panel[] = [ { id: "rps", title: "Requests", unit: "/s", values: [812, 826, 840, 833, 851, 870, 902, 915, 931, 944, 960, 958, 971, 990, 1004, 1012, 1030, 1027, 1041, 1055, 1063, 1058, 1072, 1080] }, { id: "errors", title: "Error rate", unit: "%", values: [0.4, 0.4, 0.5, 0.4, 0.4, 0.5, 0.5, 0.4, 0.6, 0.5, 0.5, 0.4, 0.5, 0.6, 0.5, 0.5, 0.4, 0.5, 0.5, 0.6, 0.5, 0.5, 0.4, 0.5] }, { id: "p95", title: "p95 latency", unit: "ms", values: [212, 218, 209, 224, 231, 226, 240, 238, 252, 261, 274, 290, 318, 342, 361, 355, 338, 322, 301, 288, 276, 269, 262, 258] }, { id: "cpu", title: "CPU", unit: "%", values: [41, 42, 44, 43, 45, 47, 48, 50, 52, 53, 55, 58, 61, 63, 62, 60, 58, 57, 55, 54, 53, 52, 52, 51] }, { id: "mem", title: "Memory", unit: "%", values: [63, 63, 64, 64, 64, 65, 65, 65, 66, 66, 66, 67, 67, 67, 68, 68, 68, 68, 69, 69, 69, 69, 70, 70] }, { id: "queue", title: "Queue depth", unit: "", values: [12, 14, 11, 15, 18, 16, 21, 24, 28, 33, 41, 52, 68, 81, 77, 64, 51, 43, 36, 30, 25, 21, 18, 16] }, ]; /** p95 open as a side panel. The picker tries the other two forms on the * same state; the range under every panel stays the same one. */ export default function Demo() { const [form, setForm] = useState
("side-panel"); const [expanded, setExpanded] = useState("p95"); return (
v && setForm(v as Form)} aria-label="Form of expansion" className="justify-start"> {FORMS.map(([id, label]) => ( {label} ))}
); }