Skip to content
KONIGI

Dashboards / Interaction / Search across panels

6 of 8

Search across panels

The dashboard has sixty panels and the viewer knows the name of the one they want.

Updated September 10, 2026

Problem

There are four hundred dashboards. Someone needs the one with the checkout latency panel. They know the word “checkout”. They do not know which folder somebody filed it under in 2023.

Solution

A search box that reaches everything and a keyboard shortcut that opens it from anywhere. Past a certain scale this stops being a convenience and becomes the primary navigation, and the folder tree becomes a filing system nobody browses.

The threshold is lower than most teams expect. Somewhere around fifty dashboards, hierarchy stops being memorable and search takes over. Every mature dashboarding product has arrived at the same place, and the tell is that Grafana’s own guidance for finding things at scale is search rather than the folder structure it also provides.

What search covers is the design decision. Dashboard titles alone is the cheapest and least useful, because the word someone remembers is usually a panel title, a metric name or a tag rather than the dashboard’s name. Reaching into panel titles, descriptions, tags and queries is much more useful and much more expensive, and it is where the difference between a search box and a real one lies.

Two behaviours separate a good implementation.

It says why each result matched. A list of dashboard names, where the match was on a panel three screens down, forces the viewer to open each one and hunt. Showing the matching panel or tag turns a list into an answer.

It is reachable without the mouse. A command palette on a keyboard shortcut collapses navigate-to-anything into two seconds, and once it exists people stop using the navigation entirely.

The complication nobody solves well is that dashboards accumulate duplicates. Search over four hundred dashboards where six are called “API Overview” returns six results, and nothing on screen says which is the one the team actually uses.

Use when

The estate is beyond what anyone can hold in their head, viewers arrive knowing a term rather than a location, and the same people navigate many times a day.

Don’t use when

There are eight dashboards. Search over a small set adds an interaction where a visible list would have been faster, and it hides the shape of what exists from anyone new.

Trade-offs

Search rewards knowing the vocabulary and punishes exploration, so a newcomer who does not know the word finds nothing and concludes it does not exist. It removes the incentive to maintain structure, which accelerates the sprawl that made search necessary. Ranking is hard and usually recency-based, which surfaces whatever somebody edited most recently rather than what is authoritative. And searching query text finds copies and forks as readily as originals, with no signal about which is canonical.

Checklist

  • What does search cover: dashboard titles, panel titles, descriptions, tags, query text?
  • Does each result say why it matched?
  • Is there a keyboard shortcut, and does it work from every page?
  • What ranks results, and does that favour authoritative over recent?
  • Are duplicates and forks distinguishable in the results?
  • Is there any signal of which dashboard a team actually uses?
  • What does a zero-result search offer next?
  • Can results be filtered by tag, folder or owner?
  • Does search reach dashboards the viewer cannot access, and how is that handled?
  • Is anything encouraging structure, or has search made sprawl free?

Compare

Grafana treats dashboard search as the real navigation at scale, with tag and folder filtering layered on, which is an honest acknowledgement that a folder tree stops working somewhere in the low hundreds. Datadog runs a global search across dashboards, monitors, logs and traces from one field, so the unit found is a resource of any type rather than a page. Sentry has a smaller estate and pushes search into the issue stream instead, where the query language does the narrowing that navigation does elsewhere. Command palettes in developer tools generally are worth studying here, because they solved the same problem—many destinations, keyboard-first, results that explain themselves—and set the expectation people now bring to dashboards.

Sidebar and canvas is the navigation search eventually replaces. Filter bar is the same narrowing applied to data rather than to destinations. Saved view is what people build once search has found the thing twice. Multi-page dashboard is the structure being searched. Explain this metric covers the vocabulary problem that makes search fail for newcomers.

Search across panels anatomy A command palette over a dimmed page. Each result says what it matched on—a panel title, a tag, a query—rather than only naming the dashboard. At the foot, six results all called API Overview, with nothing on screen saying which one the team uses. Says what it matched, not just what it found checkout p95 Payments overview panel: Checkout p95 by region Service SLOs tag: checkout Edge latency query: histogram_quantile(0.95, checkout_...) cmd K 1 2 "API overview" API Overview edited 2 years ago API Overview edited 4 months ago API Overview edited yesterday API Overview (old) edited 3 years ago 2 more 3 1 REACHABLE WITHOUT A MOUSE A palette on a shortcut collapses navigate- to-anything into two seconds, and once it exists people stop using the nav at all. 2 WHY IT MATCHED The word someone remembers is usually a panel title or a tag, not the dashboard's name. Say where the hit was. 3 THE PART NOBODY SOLVES Dashboards accumulate duplicates, and nothing on screen says which of the six is the one the team actually uses.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

Sixty panels and the reader knows the name of the one they want. Results have to say which dashboard and which row each hit lives on, because half the panel names in an organisation are the same three words.

shadcn
npx shadcn@latest add command button
npm
cmdk lucide-react
Tokens
--popover--popover-foreground--muted-foreground--border--accent
cmd K

Payments overview Payments

panel: Checkout p95 by region

412 opens · 30d

edited 3 days ago

Service SLOs Platform

tag: checkout

268 opens · 30d

edited 11 days ago

Edge latency Network

query: histogram_quantile(0.95, sum by (le, pop) (rate(checkout_edge_ttfb_seconds_bucket[5m])))

57 opens · 30d

edited 2 days ago

SearchAcrossPanels.tsxThe palette on ⌘K. Every row says which field it matched and how much the dashboard is used, and the empty state says what search covers.

import { useEffect, useState } from "react";
import { Command, CommandEmpty, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { ago, search, type Dashboard, type Hit } from "./search";

export function SearchAcrossPanels({ dashboards, open, onOpenChange, defaultQuery = "", onOpen, now }: {
  dashboards: Dashboard[];
  open: boolean;
  onOpenChange: (open: boolean) => void;
  defaultQuery?: string;
  onOpen: (hit: Hit) => void;
  /** The clock to age "edited" against. Pass one to render on a server. */
  now?: Date;
}) {
  const [query, setQuery] = useState(defaultQuery);
  const hits = search(query, dashboards);
  const at = now ?? new Date();

  // ⌘K from anywhere on the page, and the same key closes it.
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault(); onOpenChange(!open); }
      if (e.key === "Escape" && open) onOpenChange(false);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open, onOpenChange]);

  if (!open) return null;

  return (
    // In a page this sits in CommandDialog over the dimmed content; the box
    // itself is the same.
    <Command shouldFilter={false} className="relative rounded-lg border shadow-md">
      <CommandInput value={query} onValueChange={setQuery} placeholder="Search dashboards, panels, tags and queries" autoFocus />
      <kbd className="absolute right-3 top-3 rounded border px-1.5 py-0.5 text-[10px] text-muted-foreground">cmd K</kbd>
      <CommandList>
        <CommandEmpty className="px-3 py-6 text-xs text-muted-foreground">
          Nothing in a title, panel, tag or query mentions "{query}". Try one word of it, or a tag.
        </CommandEmpty>
        {hits.map((h) => (
          <CommandItem key={h.dashboard.id} value={h.dashboard.id} onSelect={() => onOpen(h)} className="flex items-start gap-3 px-3 py-2.5">
            <div className="min-w-0 flex-1">
              <p className="text-sm text-popover-foreground">{h.dashboard.title} <span className="text-[11px] text-muted-foreground">{h.dashboard.folder}</span></p>
              {/* Why it matched. The word someone remembers is usually a
                  panel or a tag, and this is where it was. */}
              <p className={`mt-0.5 truncate text-[11px] text-muted-foreground ${h.on === "query" ? "font-mono text-[10px]" : ""}`}>{h.on}: {h.text}</p>
            </div>
            <div className="shrink-0 text-right text-[10px] tabular-nums text-muted-foreground">
              <p>{h.dashboard.opens} opens · 30d</p>
              <p>edited {ago(h.dashboard.edited, at)}</p>
            </div>
          </CommandItem>
        ))}
      </CommandList>
    </Command>
  );
}

search.tsReaches titles, panels, tags and query text; a hit carries the field it was found in; ranks by terms matched, then use, then recency.

/** One dashboard as the index sees it. Everything search reaches is here,
 *  and nothing else is searchable. */
export type Dashboard = {
  id: string;
  title: string;
  folder: string;
  tags: string[];
  panels: { title: string; query: string }[];
  edited: Date;
  /** Opens in the last thirty days. The only signal of which copy a team
   *  actually uses, so it ranks above recency. */
  opens: number;
};

/** Where the hit was. Closed, and a hit cannot exist without one: a result
 *  that only names the dashboard makes the reader open it and hunt. */
export type Field = "title" | "panel" | "tag" | "query";
const ORDER: Field[] = ["title", "panel", "tag", "query"];

export type Hit = { dashboard: Dashboard; on: Field; text: string; terms: number };

const fields = (d: Dashboard): [Field, string][] => [
  ["title", d.title],
  ...d.panels.map(({ title }) => ["panel", title] as [Field, string]),
  ...d.tags.map((t) => ["tag", t] as [Field, string]),
  ...d.panels.map(({ query }) => ["query", query] as [Field, string]),
];

/** Every term is tried against every field. The field that holds the most
 *  terms is the reason shown; ties go to the field a person would remember. */
export function search(q: string, dashboards: Dashboard[]): Hit[] {
  const terms = q.toLowerCase().split(/\s+/).filter(Boolean);
  if (!terms.length) return [];
  const hits: Hit[] = [];
  for (const d of dashboards) {
    let best: Hit | null = null;
    for (const [on, text] of fields(d)) {
      const n = terms.filter((t) => text.toLowerCase().includes(t)).length;
      if (n && (!best || n > best.terms || (n === best.terms && ORDER.indexOf(on) < ORDER.indexOf(best.on))))
        best = { dashboard: d, on, text, terms: n };
    }
    if (best) hits.push(best);
  }
  // Terms matched, then use, then recency. Recency last on purpose: it
  // surfaces whatever somebody edited, which is rarely the authoritative copy.
  return hits.sort((a, b) => b.terms - a.terms || b.dashboard.opens - a.dashboard.opens || b.dashboard.edited.getTime() - a.dashboard.edited.getTime());
}

export function ago(then: Date, now: Date) {
  const days = Math.floor((now.getTime() - then.getTime()) / 86_400_000);
  if (days < 1) return "today";
  if (days === 1) return "yesterday";
  if (days < 30) return `${days} days ago`;
  if (days < 365) return `${Math.floor(days / 30)} months ago`;
  const y = Math.floor(days / 365);
  return y === 1 ? "a year ago" : `${y} years ago`;
}

demo.tsxHow it is called: five dashboards, opened on "checkout p95". Type "api" for the two copies of API Overview and the opens count that separates them.

import { useState } from "react";
import { Search } from "lucide-react";
import { Button } from "@/components/ui/button";
import { SearchAcrossPanels } from "./SearchAcrossPanels";
import type { Dashboard } from "./search";

const NOW = new Date("2026-09-15T09:00:00Z");
const daysAgo = (n: number) => new Date(NOW.getTime() - n * 86_400_000);

/** A slice of the estate. Two are called API Overview; the opens column is
 *  what tells them apart. */
const DASHBOARDS: Dashboard[] = [
  { id: "pay", title: "Payments overview", folder: "Payments", tags: ["payments"], edited: daysAgo(3), opens: 412,
    panels: [{ title: "Checkout p95 by region", query: "histogram_quantile(0.95, sum by (le, region) (rate(checkout_latency_seconds_bucket[5m])))" }, { title: "Authorisations", query: "sum(rate(payment_auth_total[5m]))" }] },
  { id: "slo", title: "Service SLOs", folder: "Platform", tags: ["checkout", "slo"], edited: daysAgo(11), opens: 268,
    panels: [{ title: "Availability", query: "1 - (sum(rate(http_5xx_total[30d])) / sum(rate(http_requests_total[30d])))" }, { title: "Latency burn rate", query: "slo:latency_burn_rate:1h" }] },
  { id: "edge", title: "Edge latency", folder: "Network", tags: ["edge", "cdn"], edited: daysAgo(2), opens: 57,
    panels: [{ title: "TTFB by POP", query: "histogram_quantile(0.95, sum by (le, pop) (rate(checkout_edge_ttfb_seconds_bucket[5m])))" }] },
  { id: "api", title: "API Overview", folder: "Platform", tags: ["api"], edited: daysAgo(120), opens: 390,
    panels: [{ title: "Requests", query: "sum(rate(http_requests_total[5m]))" }] },
  { id: "api-old", title: "API Overview", folder: "Archive", tags: ["api"], edited: daysAgo(730), opens: 2,
    panels: [{ title: "Requests", query: "sum(rate(http_requests_total[5m]))" }] },
];

/** Open on arrival with a search already typed. ⌘K closes and opens it;
 *  picking a result would navigate. Type "api" to see the duplicates. */
export default function Demo() {
  const [open, setOpen] = useState(true);
  const [went, setWent] = useState<string | null>(null);
  return (
    <div className="min-h-[220px]">
      {!open && (
        <Button variant="outline" size="sm" onClick={() => setOpen(true)}><Search className="size-3.5" /> Search <kbd className="ml-1 text-[10px] text-muted-foreground">⌘K</kbd></Button>
      )}
      <SearchAcrossPanels
        dashboards={DASHBOARDS}
        open={open}
        onOpenChange={setOpen}
        defaultQuery="checkout p95"
        onOpen={(h) => { setWent(`${h.dashboard.title}, ${h.on} ${h.text}`); setOpen(false); }}
        now={NOW}
      />
      {went && <p className="mt-2 text-xs text-muted-foreground">opened {went}</p>}
    </div>
  );
}
What it renders. Identical markup in both panes, with only the token values changing.

Examples

Captures whose hotspots reference this pattern, grouped by product and dated. The dashed boxes are this pattern; hover any box for the note.

Honeycomb

Query-first; heatmaps and BubbleUp replace the dashboard-of-panels model with draw-a-region cross-filtering.

Honeycomb — Query / HEATMAP(duration_ms)
Heatmap. Linear y-axis, so the band holding most requests is six percent of the panel height and the empty top half gets the rest. Search across panels. The schema is the navigation, and it opens with a filter box rather than a tree you're expected to browse. Dashboard builder. No panel to configure—the query is the page. WHERE trace.parent_id does-not-exist is how you say root spans only. Tabs as genres. Five readings of one result: overview, BubbleUp, correlations, traces, raw. Each tab is a different question, not a different subject. Freshness indicator. When data last arrived, not when the page last ran. The one of the three ages that actually matters. Time-range picker. Absolute, with the granularity stated beside it, and arrows that step to the previous window rather than retyping it. Data source badge. Elapsed query time and 11,710,335 rows examined. The panel reporting its own cost and scope, which almost nothing else does.
Query / HEATMAP(duration_ms) September 10, 2026 Honeycomb sandbox, public dataset (signed out; no version string exposed) medium · light · desktop-web
My heatmap entry cites Honeycomb as the argument for log-scale y-buckets, so it's worth recording that this is Honeycomb's own sandbox rendering the same chart on a linear axis. The result is the failure the argument warns about: the ticks run 0 to 3500 evenly, the dense band where almost every request actually lives is squashed into the bottom sixth of the panel, and the top half is mostly empty. What survives anyway is the thing a percentile can't tell you. The solid band under a second doesn't move across the whole window, while from about 07:00 a separate purple tail climbs to 3000ms and keeps going. Two populations, one of them fine and one of them deteriorating. A p95 line over this data would have risen and said nothing about which. The other thing worth stealing: the footer reports elapsed query time and that it examined 11,710,335 rows, so the panel tells you what it cost and how much it looked at. Cookie banner and a no-signup onboarding modal were removed to take the shot; nothing of the product's own UI was.
  • Heatmap Linear y-axis, so the band holding most requests is six percent of the panel height and the empty top half gets the rest.
  • Dashboard builder No panel to configure—the query is the page. WHERE trace.parent_id does-not-exist is how you say root spans only.
  • Tabs as genres Five readings of one result: overview, BubbleUp, correlations, traces, raw. Each tab is a different question, not a different subject.
  • Search across panels The schema is the navigation, and it opens with a filter box rather than a tree you're expected to browse.
  • Time-range picker Absolute, with the granularity stated beside it, and arrows that step to the previous window rather than retyping it.
  • Data source badge Elapsed query time and 11,710,335 rows examined. The panel reporting its own cost and scope, which almost nothing else does.
  • Freshness indicator When data last arrived, not when the page last ran. The one of the three ages that actually matters.
Show 3 more examples Hide the rest

Kibana

Query-first rather than panel-first: the search bar is the primary control and the charts are downstream of it, which inverts Grafana's arrangement.

Dashboards / list September 10, 2026 Elastic demo environment (guest session) medium · light · desktop-web
The estate, rather than a dashboard. Twenty rows a page and fourteen pages, so something close to 280 dashboards, and sixteen of the twenty on this first page are called "[Metrics Kubernetes] something"—Cronjobs, StatefulSets, Volumes, Pods, Deployments, Proxy, DaemonSets, Jobs, Nodes. They were generated by an integration rather than designed, they all landed on the same day, and they will sit in this list forever. That's the shape every mature dashboard estate takes, and it's why search stops being a convenience at this scale: nobody is browsing to page nine. The list does two things well. Each row carries a one-line description under the title, which is the difference between a name and an answer. And the bracket prefix is doing the work a folder would, so the generated ones sort together and stay out of the way of the four a person actually made.
  • Multi-page dashboard A set of peers with no landing page. Fourteen pages of them, and nothing says which is the one the team uses.
  • Search across panels Past about fifty dashboards this is the navigation and the list below it is a filing system nobody browses.
  • Semantic grouping Tags and a bracket prefix standing in for folders, which is what keeps 250 generated pages out of the way.
  • Data table Name, description, last updated, actions. The description column is what makes this a list you can read rather than scan.
Kibana — Discover / filebeat logs
Two-pane list and detail. Fields on the left, records on the right, both on screen at once. The oldest working shape for a queue there is. Data table. One column called Summary holding every field on the record. Complete, and unreadable at a glance. Histogram and distribution. Volume over the window at a 30-second auto interval. The final bar is the bucket still filling, and it always reads low. Filter bar. A query language in the bar rather than chips. Powerful, and it hides what's applied from anyone who didn't type it. Detail on demand. An expander on every row. Inline rather than a side panel, so it pushes the rest of the list down. Time-range picker. Relative by default, with the refresh control beside it rather than buried in a settings menu. Search across panels. 315 fields, so the sidebar opens with a filter box. Past a certain count a tree is a filing system nobody browses.
Discover / filebeat logs September 10, 2026 Elastic demo environment (guest session; no version string exposed) dense · light · desktop-web
Two panes: 315 fields down the left, 13,637 documents on the right, and a volume histogram over both. The left pane is the good half—it opens with a search box rather than a tree, which is the only sane way to navigate that many fields. The right pane is where it falls over. The documents table ships with two columns, a timestamp and "Summary", and Summary is every field on the record concatenated into one cell: agent.ephemeral_id, agent.id, agent.name, agent.type, agent.version, cloud.account.id, cloud.availability_zone, and on for three wrapped lines per row. It is technically complete and it cannot be scanned, so the first thing anyone does here is pick columns—which is to say the default view's job is to make you configure it. Underneath, the pager reads 100 rows per page across 137 pages, and the sort control sits above a table showing the first of them.
  • Two-pane list and detail Fields on the left, records on the right, both on screen at once. The oldest working shape for a queue there is.
  • Detail on demand An expander on every row. Inline rather than a side panel, so it pushes the rest of the list down.
  • Data table One column called Summary holding every field on the record. Complete, and unreadable at a glance.
  • Search across panels 315 fields, so the sidebar opens with a filter box. Past a certain count a tree is a filing system nobody browses.
  • Filter bar A query language in the bar rather than chips. Powerful, and it hides what's applied from anyone who didn't type it.
  • Time-range picker Relative by default, with the refresh control beside it rather than buried in a settings menu.
  • Histogram and distribution Volume over the window at a 30-second auto interval. The final bar is the bucket still filling, and it always reads low.

Netdata

Per-second charts, hundreds per node, with a per-chart anomaly ribbon instead of a band on the series.

Netdata — Metrics / System
Header KPI strip. Twelve tiles in two rows, in four different layouts. It reads as twelve things rather than one strip. Sidebar and canvas. The rail is the dashboard. A generated tree of every metric family the agent found. Gauge and dial. Arcs for disk reads and writes, where the maximum is invented—this is a rate, not a capacity. Dashboard builder. A query builder inline in the panel header: group by, aggregation, node and dimension count. Search across panels. 720 charts, so search is the navigation and the tree is the filing system nobody browses. Semantic grouping. Sections generated by the collector rather than chosen. Consistent, and about nothing in particular. Compare periods. Offered per chart rather than per dashboard, which is the scoping trap: one panel shifted, the rest not. Freshness indicator. Live 1, Stale 8. Most products would have drawn all nine and said nothing.
Metrics / System September 10, 2026 Netdata Agent v2.10.0-686-nightly (public registry node, signed out) dense · dark · desktop-web
Structurally the opposite of Grafana, and worth the comparison. Nobody built this page. The right rail says "showing 720 of total 720 charts" and the tree beneath it—System, Compute, Memory, Storage, Network, Hardware, Processes, then Apps, Users, Groups, O/S Services, and every application it found—is generated from what the agent collects. The canvas is the same hierarchy rendered downward, four levels deep, headings prefixed with dashes: System, then Compute, then CPU, then the chart. There is no editorial layer at all, which means nothing is missing and nothing is prioritised. The header also does something most products won't: it reports "Live 1, Stale 8" beside the node count, so eight of the nine machines behind this view are not currently reporting and the page says so rather than drawing their last known values.
  • Sidebar and canvas The rail is the dashboard. A generated tree of every metric family the agent found.
  • Search across panels 720 charts, so search is the navigation and the tree is the filing system nobody browses.
  • Header KPI strip Twelve tiles in two rows, in four different layouts. It reads as twelve things rather than one strip.
  • Gauge and dial Arcs for disk reads and writes, where the maximum is invented—this is a rate, not a capacity.
  • Semantic grouping Sections generated by the collector rather than chosen. Consistent, and about nothing in particular.
  • Freshness indicator Live 1, Stale 8. Most products would have drawn all nine and said nothing.
  • Dashboard builder A query builder inline in the panel header: group by, aggregation, node and dimension count.
  • Compare periods Offered per chart rather than per dashboard, which is the scoping trap: one panel shifted, the rest not.