Skip to content
KONIGI Patterns

Dashboards / Dashboard features / Alert rule attached to panel

1 of 10 ›

Alert rule attached to panel

People need a notification when a metric requires attention.

Updated September 10, 2026

Problem

People watching a metric need to know when it requires attention without keeping the dashboard open.

Solution

Let people create an alert from the chart using the same query and threshold. Keep the relationship visible so later edits do not leave the chart and alert describing different conditions.

Ask how long the condition must last, who receives the alert, and what should happen when data stops arriving. Make the expected response clear. An alert should help someone decide what to do.

Show existing rules before creating another one, and explain what happens to a rule when its panel is changed or deleted.

Use when

The metric has a meaningful alert condition and someone is responsible for responding.

Don’t use when

Nobody would act on it. An alert nobody can respond to trains a team to ignore the channel, and the cost lands on the next alert rather than this one. Also avoid it when the condition needs several signals: a per-panel rule encourages single-metric thinking.

Trade-offs

Rules attached to individual panels can multiply across dashboards and send duplicate notifications. Static thresholds need review as conditions change. Missing data requires its own behavior so silence is not mistaken for normal operation.

Checklist

  • Is this symptom or cause, and if cause, is it definite and imminent?
  • Could a person act on this at 3am, and would the action be more than restarting something?
  • Is there a duration condition, or does a single spike page?
  • What happens on no data, and was that chosen deliberately?
  • Does the rule use the same query as the panel, or a copy that can drift?
  • Who owns this rule, and do they know?
  • Does the same condition exist on another dashboard?
  • What happens to the rule if the panel or dashboard is deleted?
  • Is the threshold still right for current traffic, and when was that checked?
  • How often has this fired, and how often did it lead to action?

Compare

Grafana creates alert rules from panels and forces the no-data decision at rule-creation time with four explicit mappings, which is the most honest treatment of the failure mode most products leave implicit. Prometheus keeps rules in version-controlled config rather than in a UI, so they get reviewed, diffed and owned like code, at the cost of the create-it-while-looking-at-it immediacy this pattern is named for. Datadog makes the monitor the primary object and lets dashboards reference it, which inverts the relationship and stops the same condition being redefined per dashboard. Sentry derives alerting from issue behaviour—new, regressed, spiking—rather than from a threshold on a line, which sidesteps threshold staleness entirely and only works because its data has a natural notion of “new”.

Threshold line shows the alert boundary. Metric targets records the goal and its owner. Error and stale state handles missing or failed data.

Semantic status color shows an active alert. Status history records changes in alert state.

Alert rule attached to panel anatomy A panel with a threshold drawn on it and the rule created from it, carrying the same query. The rule form asks for a duration as well as a value, asks what to do when no data arrives, and asks who it goes to. The line you drew becomes the condition Checkout error rate 1% New rule condition error rate > 1% for 5 minutes on no data alerting, not normal routes to payments on-call 1 2 3 1 ONE OBJECT, NOT TWO A rule defined elsewhere drifts from the chart inside a quarter, and then there are two numbers: the one on the wall and the one that wakes somebody. 2 DURATION, NOT JUST VALUE The easiest rule to create is a static threshold on a cause-shaped metric, which is exactly the page nobody needed. Make the useful rule as easy as the tempting one. 3 WHAT NO DATA MEANS A rule that can't tell "the metric is fine" from "the metric stopped arriving" will be silent during exactly the outage it was written for. Every page should be actionable. If it merits a robotic response, it shouldn't be a page.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

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.

shadcn
npx shadcn@latest add card input select
npm
recharts
Tokens
--card--card-foreground--muted-foreground--border--status-critical--status-warn--chart-1

Checkout error rate

1%

New rule

conditionerror rate > 1%for5 minuteson no dataroutes topayments on-call

AlertRule.tsxThe condition is computed from the panel, the threshold is drawn on the panel's axes, and no-data is a closed choice with four values.

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<NoData, string> = {
  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 (
    <div className="grid gap-5 sm:grid-cols-[minmax(0,1fr)_minmax(0,1.15fr)]">
      <Card className="p-4">
        <p className="border-b pb-2 text-xs uppercase tracking-wide text-muted-foreground">{panel.title}</p>
        <div className="mt-3 overflow-x-auto">
          <LineChart width={width} height={height} data={data} margin={{ top: 8, right: 8, bottom: 0, left: 8 }}>
            <XAxis interval={0} dataKey="i" hide />
            <YAxis domain={[0, top]} hide />
            {/* 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. */}
            <ReferenceLine y={rule.threshold} stroke={CRITICAL} strokeWidth={2} strokeDasharray="5 4"
              label={{ value: `${rule.threshold}${panel.unit}`, position: "insideTopRight", fill: CRITICAL, fontSize: 10 }} />
            <Line type="linear" dataKey="value" stroke="hsl(var(--chart-1))" strokeWidth={2} dot={false} isAnimationActive={false} />
          </LineChart>
        </div>
      </Card>

      <Card className="p-4">
        <p className="border-b pb-2 text-xs uppercase tracking-wide text-muted-foreground">New rule</p>
        <div className="mt-3 grid grid-cols-[84px_1fr] items-center gap-x-3 gap-y-2.5 text-xs">
          <span className="text-muted-foreground">condition</span>
          <span className="flex items-center justify-end gap-2 font-mono text-card-foreground">
            {condition(panel, rule)}
            <Input type="number" step="0.1" value={rule.threshold} aria-label={`threshold in ${panel.unit}`} className={field}
              onChange={(e) => onChange({ ...rule, threshold: Number(e.target.value) })} />
          </span>

          <span className="text-muted-foreground">for</span>
          <span className="flex items-center justify-end gap-2 tabular-nums text-card-foreground">
            {rule.forMinutes} minutes
            <Input type="number" min={1} value={rule.forMinutes} aria-label="minutes the condition must hold" className={field}
              onChange={(e) => onChange({ ...rule, forMinutes: Number(e.target.value) })} />
          </span>

          <span className="text-muted-foreground">on no data</span>
          <span className="flex justify-end">
            <Select value={rule.noData} onValueChange={(v) => onChange({ ...rule, noData: v as NoData })}>
              <SelectTrigger aria-label="what no data means"
                className={`h-7 w-auto gap-2 text-xs ${rule.noData === "alerting" ? "text-status-warn" : "text-card-foreground"}`}>
                <SelectValue>{NO_DATA_LABEL[rule.noData]}</SelectValue>
              </SelectTrigger>
              <SelectContent>
                {(Object.keys(NO_DATA_LABEL) as NoData[]).map((k) => (
                  <SelectItem key={k} value={k} className="text-xs">{NO_DATA_LABEL[k]}</SelectItem>
                ))}
              </SelectContent>
            </Select>
          </span>

          <span className="text-muted-foreground">routes to</span>
          <span className="text-right text-card-foreground">{rule.routeTo}</span>
        </div>
      </Card>
    </div>
  );
}

demo.tsxHow it is called: the panel's series and a rule at 1% for 5 minutes, alerting on no data.

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<Rule>({
    threshold: 1,
    forMinutes: 5,
    noData: "alerting",
    routeTo: "payments on-call",
  });
  return <AlertRule panel={PANEL} rule={rule} onChange={setRule} />;
}
What it renders. Identical markup in both panes, with only the token values changing.

Examples

Screenshots are grouped by product and include the capture date. Dashed boxes highlight this pattern. Notes appear below each screenshot and when you hover over a box.

Grafana

Examples of panel grids, shared selectors, stat tiles, and chart configuration.

Examples / Alert List September 10, 2026 Grafana Play (signed out; no version string exposed) medium · dark · desktop-web
This list shows seven alerts with their states and durations. Several have been firing for days, which raises a question about whether the rules still prompt a useful response. The names use inconsistent conventions and often do not describe the affected system clearly. Pending and firing states are labeled separately, and each row links back to its rule.