Template variable One dashboard parameterised by service, cluster or tenant. The variables belong at the top in reading order, and a chained variable has to reset its children when the parent changes or the page shows a combination that never existed. npx shadcn@latest add select Tokens this needs: --card, --card-foreground, --muted, --muted-foreground, --border, --input, --status-warn The status, chart, scale, state and direction names are an extension, not a rename. shadcn has --destructive and five --chart-* and nothing else in this territory. ──────────────────────────────────────────────────────────────────────── // TemplateVariables.tsx ──────────────────────────────────────────────────────────────────────── import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { cn } from "@/lib/utils"; export type Variable = { name: string; /** The query that populates it, run against the data, so the list is * never stale and never hand-maintained. */ query: string; /** What that query returned. Absent while it is still running. */ options?: string[]; /** A chained variable resets when its parent changes, so the page never * shows a combination that never existed. */ dependsOn?: string; allowAll?: boolean; }; export type Panel = { title: string; query: string }; export type Values = Record; type Props = { variables: Variable[]; values: Values; onChange: (values: Values) => void; /** Every panel names the variable, never a literal. The layout is written once. */ panels: Panel[]; }; export const ALL = "all"; /** $service in a title or a query becomes the chosen value, so a screenshot * says what it is a screenshot of. */ export const interpolate = (text: string, values: Values) => text.replace(/\$(\w+)/g, (_, name) => values[name] ?? `$${name}`); /** The chosen values as a query string, so a scoped view is a link. */ export const toSearch = (values: Values) => "?" + Object.entries(values).filter(([, v]) => v && v !== ALL).map(([k, v]) => `var-${k}=${encodeURIComponent(v)}`).join("&"); export function TemplateVariables({ variables, values, onChange, panels }: Props) { const set = (name: string, value: string) => { const next = { ...values, [name]: value }; const reset = (parent: string) => { for (const v of variables) if (v.dependsOn === parent) { next[v.name] = v.allowAll ? ALL : ""; reset(v.name); } }; reset(name); onChange(next); }; return (
{/* In reading order, above everything, on every screen size. */}
{variables.map((v) => { const value = values[v.name] ?? ""; const loading = !v.options; const gone = !loading && value !== ALL && !v.options!.includes(value); const shown = loading ? "loading…" : value === ALL ? `all (${v.options!.length})` : gone ? `${value} (gone)` : value; return ( ); })}
{panels.map((p, i) => (

{interpolate(p.title, values)}

{p.query}
))}
{toSearch(values)}
); } ──────────────────────────────────────────────────────────────────────── // demo.tsx ──────────────────────────────────────────────────────────────────────── import { useState } from "react"; import { TemplateVariables, type Values, type Variable } from "./TemplateVariables"; /** * Three chained variables and three panels that name them. Service depends on * env, host on service; change env and the two below reset. The URL at the * bottom is what a real app would push into history on every change. */ const VARIABLES: Variable[] = [ { name: "env", query: "label_values(env)", options: ["prod", "staging", "dev"] }, { name: "service", query: "label_values(service)", dependsOn: "env", options: ["checkout", "search", "cart", "auth", "feed", "billing", "catalog", "email", "export", "gateway"], }, { name: "host", query: "label_values(up{service=\"$service\"}, instance)", dependsOn: "service", allowAll: true, options: Array.from({ length: 40 }, (_, i) => `checkout-${String(i + 1).padStart(2, "0")}`), }, ]; const PANELS = [ { title: "$service error rate", query: "rate($service errors)" }, { title: "$service p95 on $host", query: "p95($service, $host)" }, { title: "pods up in $env", query: "sum by (pod) (up{env=\"$env\"})" }, ]; export default function Demo() { const [values, setValues] = useState({ env: "prod", service: "checkout", host: "all" }); return (
); }