Alert rule attached to panel The chart shows the line to watch and the rule lives on it. Drawing the threshold on the same axes as the data is the only way a reader can see how close the current value is to firing. npx shadcn@latest add card input select npm i recharts Tokens this needs: --card, --card-foreground, --muted-foreground, --border, --status-critical, --status-warn, --chart-1 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. ──────────────────────────────────────────────────────────────────────── // AlertRule.tsx ──────────────────────────────────────────────────────────────────────── import { Line, LineChart, ReferenceLine, XAxis, YAxis } from "recharts"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; /** * The rule is created from the panel and carries the panel's query, so the * line on the chart and the condition that pages someone are one object. * Nothing here lets the two be typed separately. */ export type Panel = { title: string; /** The metric as the rule will name it: "error rate", "p95 latency". */ metric: string; unit: string; series: number[]; }; /** Grafana's four. A rule that cannot say what silence means is silent during * the outage it was written for, so this is a required choice, not a default. */ export type NoData = "alerting" | "normal" | "no-data" | "keep-last"; export const NO_DATA_LABEL: Record = { alerting: "alerting, not normal", normal: "normal", "no-data": "no data", "keep-last": "keep last state", }; export type Rule = { threshold: number; /** Minutes the condition must hold. A single spike is not a page. */ forMinutes: number; noData: NoData; routeTo: string; }; /** "error rate > 1%": computed from the panel, never typed into the rule. */ export const condition = (panel: Panel, rule: Rule) => `${panel.metric} > ${rule.threshold}${panel.unit}`; const CRITICAL = "hsl(var(--status-critical))"; export function AlertRule({ panel, rule, onChange, width = 280, height = 150 }: { panel: Panel; rule: Rule; onChange: (rule: Rule) => void; width?: number; height?: number; }) { const data = panel.series.map((value, i) => ({ i, value })); const top = Math.max(rule.threshold, ...panel.series) * 1.15; const field = "h-7 w-24 text-right text-xs tabular-nums"; return (

{panel.title}

{/* The threshold on the data's own axes. This is the whole reason the rule lives on the panel: the reader can see how close it is. */}

New rule

condition {condition(panel, rule)} onChange({ ...rule, threshold: Number(e.target.value) })} /> for {rule.forMinutes} minutes onChange({ ...rule, forMinutes: Number(e.target.value) })} /> on no data routes to {rule.routeTo}
); } ──────────────────────────────────────────────────────────────────────── // demo.tsx ──────────────────────────────────────────────────────────────────────── import { useState } from "react"; import { AlertRule, type Rule } from "./AlertRule"; /** * The checkout error rate over the last eight samples, crossing 1% twice. * Changing the threshold moves the line on the chart, because the rule and * the panel are the same object. */ const PANEL = { title: "Checkout error rate", metric: "error rate", unit: "%", series: [0.24, 0.33, 0.27, 0.48, 1.12, 1.21, 0.61, 0.7], }; export default function Demo() { const [rule, setRule] = useState({ threshold: 1, forMinutes: 5, noData: "alerting", routeTo: "payments on-call", }); return ; }