Skip to content
KONIGI Patterns

Dashboards / Labels and context / Data source badge

‹ 2 of 6 ›

Data source badge

People need to know where a panel’s data comes from and whether that source is working.

Updated September 10, 2026

Problem

Similar numbers can disagree because they come from different systems. People need to know the source before deciding which one to use.

Solution

Show a short, recognizable source name on the panel. Make the underlying query or calculation available when people need to investigate a discrepancy.

Distinguish source identity from source health and data age. A reachable source can still contain delayed data. Show relevant problems without making people open a tooltip.

Use names the audience understands, with technical identifiers available in the details.

Use when

A dashboard mixes data sources or a metric is available from more than one system.

Don’t use when

Every panel on the page reads from one source and everyone knows it. Then the badge is repeated noise, and saying it once at page level is enough.

Trade-offs

Badges use space even when people rarely consult them. Internal source names may be meaningless to the audience. Health checks and query details also need accurate labels so they do not imply more certainty than they provide.

Checklist

  • Does the panel say which source it read from?
  • Is the source name meaningful to this audience, or only to the person who configured it?
  • Does the badge carry health as well as identity?
  • If the source is lagging but responding, does anything on the page say so?
  • Can a viewer reach the query behind the number?
  • Do two panels showing the same metric from different sources make that visible?
  • Is the badge redundant on a single-source page, and could it be stated once?
  • Does the health check have its own failure mode, and what does the badge show then?
  • Is the badge reachable by screen reader, or is it an icon with a tooltip?
  • When a source is renamed or replaced, what updates the badges?

Compare

Grafana treats data sources as first-class configured objects that panels reference by name, which makes mixed-source dashboards easy to build and makes provenance entirely a matter of whether the author chose to surface it. Datadog mostly removes the question by owning ingestion, so the interesting provenance is the tag scope rather than the connector, and its UI reflects that. Honeycomb collapses source and query into one thing—you are always looking at a dataset you named in the query—so provenance is never ambiguous and never needs a badge. Looker pushes hardest in the other direction with a modelling layer, so a metric has one definition and the badge question becomes “which model version”, which is a better question and a harder one to answer in a corner of a panel.

Freshness indicator shows when data was updated. Error and stale state marks failed or outdated results. Explain this metric defines the calculation.

Empty state explains absent results. Alert rule can notify someone when a source stops supplying data.

Data source badge anatomy Two adjacent panels that look equally authoritative. Each carries a badge naming its source; one badge also reports that its source is twenty minutes behind, which nothing else on the healthy-looking chart would have said. Two panels, two sources, one page Requests per second prom-prod view query Active users finance-sheet 20m view query 1 2 3 1 IDENTITY Which configured source this is. Cheapest, most useful, almost always enough. 2 HEALTH Reachable and current. A healthy-looking chart from a source twenty minutes behind is the case nothing else catches. 3 DERIVATION What was actually asked. The one that ends disputes, and the one most products file under advanced. One dashboard can mix four sources in adjacent panels. Nothing rendered says so.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

Where a panel's numbers come from, and whether that source is healthy. Two panels side by side can be drawn from different systems with different lags, and nothing on a chart says so.

shadcn
npx shadcn@latest add badge button popover card
Tokens
--card--foreground--muted-foreground--border--state-live--state-stale--status-critical--status-unknown
Requests per second
prom-prod
Active users
finance-sheet 20m

SourceBadge.tsxSource name, a health dot computed from the age of the newest sample, and the query behind the number in a popover.

import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { freshness, FRESHNESS_COLOR } from "../freshness-indicator/freshness";

/**
 * A configured source, the way Grafana holds one: a name a viewer recognises,
 * the arrival time of its newest sample, the cadence it should arrive at, and
 * the query that was run against it.
 */
export type Source = {
  name: string;
  lastArrival: Date | null;
  expectedEveryMs: number;
  query?: string;
};

type Props = {
  source: Source;
  /** The clock to age against. Pass one to render on a server. */
  now?: Date;
  /** Where "view query" goes when the query is too long for a popover. */
  onViewQuery?: (source: Source) => void;
};

/** 20m, 1h 05m. Rounded down: a lag is never reported shorter than it is. */
const lag = (ms: number) => {
  const m = Math.floor(ms / 60_000);
  const h = Math.floor(m / 60);
  return h ? `${h}h ${String(m % 60).padStart(2, "0")}m` : `${m}m`;
};

/**
 * Identity, health and derivation on one pill. Identity is always shown.
 * Health is computed from the sample age, so a source that answers every
 * request but has nothing new to say cannot look current. Derivation opens
 * inline, because a query behind a settings gate ends no disputes.
 */
export function SourceBadge({ source, now = new Date(), onViewQuery }: Props) {
  const { state, ageMs } = freshness(source.lastArrival, source.expectedEveryMs, now);
  const behind = state === "late" || state === "stale";
  const text = behind && ageMs !== null ? `${source.name} ${lag(ageMs)}` : source.name;
  const said =
    state === "unknown" ? `${source.name}, no samples` :
    behind ? `${source.name}, ${lag(ageMs ?? 0)} behind` :
    `${source.name}, current`;

  return (
    <span className="inline-flex items-center gap-2">
      <Badge
        variant="outline"
        aria-label={said}
        title={said}
        className={cn(
          "gap-1.5 rounded-full px-2 py-0.5 text-[10px] font-normal text-muted-foreground",
          state === "late" && "border-state-stale text-state-stale",
          state === "stale" && "border-status-critical text-status-critical",
          state === "unknown" && "border-status-unknown text-status-unknown",
        )}
      >
        <span className="size-1.5 rounded-full" style={{ background: FRESHNESS_COLOR[state] }} aria-hidden="true" />
        {text}
      </Badge>

      {source.query && (
        <Popover modal={false}>
          <PopoverTrigger asChild>
            <Button variant="outline" size="sm" className="h-5 rounded-md px-2 text-[10px] text-muted-foreground" onClick={() => onViewQuery?.(source)}>
              view query
            </Button>
          </PopoverTrigger>
          <PopoverContent align="end" className="w-80 p-3">
            <p className="text-[10px] uppercase tracking-wide text-muted-foreground">{source.name}</p>
            <pre className="mt-2 whitespace-pre-wrap font-mono text-[11px] leading-relaxed text-foreground">{source.query}</pre>
          </PopoverContent>
        </Popover>
      )}
    </span>
  );
}

demo.tsxHow it is called: two panels, one on a current Prometheus, one on a sheet twenty minutes behind.

import { Card } from "@/components/ui/card";
import { Sparkline } from "../sparkline/Sparkline";
import { SourceBadge, type Source } from "./SourceBadge";

/**
 * Two panels on one page, each with its own source. Prometheus is current.
 * The finance sheet is polled every ten minutes and its newest row is twenty
 * minutes old, so its badge says so. The clock is fixed so both badges render
 * the same on the server and in the browser.
 */
const NOW = new Date("2026-09-15T09:00:00Z");
const ago = (minutes: number) => new Date(NOW.getTime() - minutes * 60_000);

const PROM: Source = {
  name: "prom-prod",
  lastArrival: ago(0.25),
  expectedEveryMs: 15_000,
  query: 'sum(rate(http_requests_total{job="api", env="prod"}[5m]))',
};
const SHEET: Source = {
  name: "finance-sheet",
  lastArrival: ago(20),
  expectedEveryMs: 10 * 60_000,
  query: "SELECT date, active_users FROM 'Active users'!A:B ORDER BY date",
};

function Panel({ title, source, values }: { title: string; source: Source; values: number[] }) {
  return (
    <Card className="p-4">
      <div className="flex items-start justify-between gap-2 border-b pb-2">
        <span className="text-[11px] uppercase tracking-wide text-muted-foreground">{title}</span>
        <SourceBadge source={source} now={NOW} />
      </div>
      <div className="mt-3 text-muted-foreground">
        <Sparkline values={values} width={268} height={60} />
      </div>
    </Card>
  );
}

export default function Demo() {
  return (
    <div className="grid grid-cols-2 gap-5">
      <Panel title="Requests per second" source={PROM} values={[32, 44, 36, 60, 52, 74, 78]} />
      <Panel title="Active users" source={SHEET} values={[40, 48, 42, 56, 50, 62, 66]} />
    </div>
  );
}
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.

Honeycomb

Query-driven views use heatmaps and selected regions to investigate differences between requests.

Query / HEATMAP(duration_ms) September 10, 2026 Honeycomb sandbox, public dataset (signed out; no version string exposed) medium · light · desktop-web
This sandbox heatmap uses a linear vertical scale. Most requests sit near the bottom, while a separate group of slower requests extends upward later in the period. The view exposes variation that a single summary value would hide, though a different scale could give the lower band more room. The footer reports query duration and rows examined. A cookie banner and onboarding dialog were removed for the capture; the dashboard itself was left intact.
Show 1 more example Hide the rest

Grafana

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

Linux node / fleet overview September 9, 2026 Grafana Play (signed out; no version string exposed) medium · dark · desktop-web
The data source was returning no measurements when this overview was captured. A red tile says no metrics were received and suggests checking configuration. The freshness tile says No data, while another panel says No errors. Those messages need clearer distinctions: missing measurements do not establish that a system has no errors.