import { useQuery } from "@tanstack/react-query" import { Link } from "@tanstack/react-router" import { LayoutDashboard } from "lucide-react" import type { DashboardDef_Output, DashboardSummary, Placement, WidgetDef, } from "@/client" import { dashboardQueryOptions } from "@/components/Dashboard/queries" import { Skeleton } from "@/components/ui/skeleton" import { cn } from "@/lib/utils" /** Columns a dashboard is cut into when its document does not say. */ const DEFAULT_COLUMNS = 12 /** * How many tiles are worth a request of their own. * * ponytail: the list endpoint carries no placements, so a footprint means * reading that dashboard's document — cheap for the handful an installation * has, and shared with the editor's own cache. The ceiling is an installation * with dozens: the tiles past this show their name and nothing else, and the * fix would be a stored footprint on `DashboardSummary`. */ const PREVIEWS = 8 /** Widgets that draw a shape, and widgets that are controls. The rest read out. */ const GRAPHIC = new Set(["chart", "forecast", "bar", "gauge"]) const INPUT = new Set([ "button", "switch", "slider", "input", "dropdown", "color", ]) const shade = (type: string) => INPUT.has(type) ? "bg-muted-foreground/30" : GRAPHIC.has(type) ? "bg-primary/45" : "bg-primary/20" /** * Where a widget sits, at the width a panel is arranged for. * * The same three-line fallback as `Dashboard/DashboardView`, written out again * rather than imported: that module pulls the whole dashboard chunk, and this * draws a schematic on a screen that shows no dashboards. */ const placement = (widget: WidgetDef): Placement => { const layout = (widget.layout ?? {}) as Record return layout.lg ?? layout.md ?? layout.sm ?? {} } type Block = { id: string type: string x: number y: number w: number h: number } /** The dashboard's widgets as one grid. */ function blocksOf(dashboard: DashboardDef_Output): { blocks: Block[] rows: number } { const blocks: Block[] = [] let rows = 0 for (const widget of dashboard.widgets ?? []) { const { x = 0, y = 0, w = 3, h = 2 } = placement(widget) blocks.push({ id: widget.id, type: widget.type, x: Math.max(0, x), y: Math.max(0, y), w: Math.max(1, w), h: Math.max(1, h), }) rows = Math.max(rows, Math.max(0, y) + Math.max(1, h)) } return { blocks, rows: Math.max(1, rows) } } /** * What a dashboard looks like from across the room: its widgets as blocks, * shaded by what kind of thing each one is. * * A footprint rather than a live render. Nothing here subscribes to a message * or reads a value — recognising "the one with the big chart on the left" is * the whole job, and it has to cost nothing on a screen that is not the * dashboard. */ function Footprint({ dashboard }: { dashboard: DashboardDef_Output }) { const { blocks, rows } = blocksOf(dashboard) const columns = dashboard.columns || DEFAULT_COLUMNS // Before the editor could place things, every widget was written at 0,0; // honouring that would pile the whole page onto one cell. const placed = blocks.some((block) => block.x > 0 || block.y > 0) const area = blocks.reduce((sum, block) => sum + block.w * block.h, 0) if (blocks.length === 0) { return (
) } return (
{blocks.map((block) => { const width = Math.min(columns, block.w) return ( ) })}
) } /** * One dashboard in the mosaic. * * Its own query, so the tiles fill in as their documents arrive instead of the * whole panel waiting for the slowest of them. */ function Tile({ dashboard, preview, }: { dashboard: DashboardSummary /** Read the document for a footprint, or settle for the name alone. */ preview: boolean }) { // The working copy, which is what the list itself is a summary of, so the // preview shows what an editor would open rather than the last publish. const { data, isPending } = useQuery({ ...dashboardQueryOptions(dashboard.name, true), enabled: preview, }) return ( {preview && isPending ? ( ) : data ? ( ) : (
)} {dashboard.title || dashboard.name} {dashboard.has_draft ? ( Unpublished changes ) : null} ) } /** * Which of two documents was worked on more recently. * * ponytail: neither `FlowSummary` nor `DashboardSummary` carries a modified * time, so this reads the two things that come close — an unpublished edit is * the one someone has open, and a higher version counter has been saved more * often. An `updated_at` on both summaries is what would make it exact. */ export function byRecency< T extends { name: string; has_draft?: boolean; version?: number }, >(a: T, b: T): number { return ( Number(b.has_draft ?? false) - Number(a.has_draft ?? false) || (b.version ?? 0) - (a.version ?? 0) || a.name.localeCompare(b.name) ) } /** The dashboards, as the shapes they are, beside the flows on the home view. */ export function DashboardMosaic({ dashboards, isPending, }: { dashboards: DashboardSummary[] isPending: boolean }) { if (isPending) { return (
{Array.from({ length: 2 }).map((_, index) => ( ))}
) } if (dashboards.length === 0) { return (

Dashboards you build show up here, as the shapes they are.

Go to dashboards
) } return (
{dashboards.map((dashboard, index) => ( ))}
) }