Skip to content

Dashboards and Panels

PropDefaultNotes
titlerequired
uidexport name as a uid1-40 letters, digits, -, _ (GRAF001, GRAF106)
description, tagsnone, []
time{ from: "now-6h", to: "now" }
refresh, timezone, weekStart, fiscalYearStartMonth, liveNow, timepickerGrafana’s defaults
graphTooltip"default""sharedCrosshair" or "sharedTooltip"
editabletrue
variables[]variable entities, in dropdown order
panels[]panels, rows and library panels, top to bottom
links[]{ title, type, url?, … }; the rest of Grafana’s link fields are filled in
annotations[]annotation queries: { name, datasource?, target?, … }; enable defaults to true, iconColor to red
folderGenerala path (a subdirectory of dashboards/; "Platform/Kubernetes" nests on Grafana 13.1 and later) or a Folder, which pins the folder’s uid (see Provisioning)
schemaVersionthe pinned schema’schant import sets it for a dashboard saved by an older Grafana, so Grafana still migrates it on load

The emitted JSON carries schemaVersion from the pinned dashboard schema unless the dashboard sets its own, the declared annotations (Grafana adds its built-in “Annotations & Alerts” one on load), and no numeric id.

An annotation query draws events on the dashboard’s time series panels: deploys, incidents, alert state changes. Its datasource takes whatever a panel’s does, and its target is the query in the datasource plugin’s shape or a query entity of that plugin:

export const checkout = new Dashboard({
title: "Checkout",
annotations: [
{ name: "Deploys", datasource: prometheus, target: new PromQuery({ expr: 'changes(kube_deployment_status_observed_generation{deployment="checkout"}[5m]) > 0' }), titleFormat: "deploy" },
{ name: "Incidents", datasource: { type: "grafana", uid: "-- Grafana --" }, iconColor: "orange", target: { type: "tags", tags: ["incident"], limit: 100, matchAny: false } },
],
});

Leave out Grafana’s built-in “Annotations & Alerts” query: Grafana adds it to every dashboard without one. Declare it (with builtIn: 1) only to change it, for example to turn it off. GRAF101 and GRAF102 check an annotation’s datasource like a panel’s, GRAF103 its variables, and GRAF108 the PromQL of one sent to Prometheus.

ClassGrafana typeDefault size (w×h)Typed from
TimeSeriesPaneltimeseries12×8timeseries options and field config
StatPanelstat6×4stat options
GaugePanelgauge6×6gauge options
TablePaneltable12×8table options and field config
LogsPanellogs24×10logs options
TracesPaneltraces24×12no options schema
HeatmapPanelheatmap12×8heatmap options and field config
TextPaneltext24×3text options
BarChartPanelbarchart12×8barchart options and field config
BarGaugePanelbargauge12×8bargauge options
PieChartPanelpiechart8×8piechart options and field config
StateTimelinePanelstate-timeline24×8statetimeline options and field config
StatusHistoryPanelstatus-history24×8statushistory options and field config
HistogramPanelhistogram12×8histogram options and field config
NodeGraphPanelnodeGraph24×12nodegraph options
XYChartPanelxychart12×8xychart options and field config
TrendPaneltrend12×8trend options and field config
CanvasPanelcanvas12×10canvas options
GeomapPanelgeomap12×10geomap options
CandlestickPanelcandlestick12×8candlestick options and field config
AnnotationsListPanelannolist8×10annotationslist options
DashboardListPaneldashlist8×10dashboardlist options
NewsPanelnews8×10news options
DataGridPaneldatagrid12×8datagrid options (Grafana 12.4 only)
FlameGraphPanelflamegraph24×12FlameGraphOptions, typed by hand
AlertListPanelalertlist8×10AlertListOptions, typed by hand

Every panel takes title, description, gridPos, datasource, targets, options, fieldConfig, transformations, links, repeat (a variable or its name), repeatDirection, maxPerRow, maxDataPoints, interval, timeFrom, timeShift, hideTimeOverride, transparent, pluginVersion and id.

options and fieldConfig.defaults.custom are the panel plugin’s own types from the pinned schema, with every field optional: Grafana fills in what a panel leaves out. schema.timeseries.Options and the other namespaces under schema are exported for anyone who wants the full types.

The alert list and flame graph panels define their options in Grafana’s TypeScript, with no CUE kind, so there is no schema to generate from. AlertListOptions and FlameGraphOptions are written by hand from Grafana 13.2.2’s source and exported. GRAF107 checks these two panels, and the traces panel, against the dashboard’s Panel definition but not their options.

transformations is typed per transformer. The dashboard schema only says a transformation has an id and some options, and foundation-sdk v0.0.20 has no schema for any transformer, so the option types are transcribed from the transformers Grafana v13.2.2 registers: packages/grafana-data/src/transformations/transformers/ for the standard ones, public/app/features/transformers/ for the rest. All 42 ids are covered, from organize, reduce, joinByField and calculateField to spatial and regression. Every top-level option is optional, because Grafana fills in each transformer’s defaults before it runs.

import { TablePanel, transformation, customTransformation } from "@intentius/chant-lexicon-grafana";
export const pods = new TablePanel({
title: "Pods",
transformations: [
{ id: "labelsToFields", options: { mode: "columns", keepLabels: ["pod", "namespace"] } },
transformation("organize", { excludeByName: { Time: true }, renameByName: { pod: "Pod" } }),
transformation("sortBy", { sort: [{ field: "Pod" }] }, { disabled: false }),
customTransformation("my-org-transformer", { level: 2 }),
],
});

An object literal is checked against its id: an unknown id, a misspelt option or a bad reducer id is a type error. There is one gap. For an option key that another transformer takes (fields belongs to reduce and groupBy, for example), tsc checks the literal against every transformer at once, so a wrong transformer’s key can slip through. transformation(id, options, common?) checks against the one transformer named, so prefer it. For a plugin’s transformer, or options newer than Grafana 13.2.2, use customTransformation(id, options, common?), which takes anything and writes it unchanged. common holds disabled, filter and topic.

A query uses its own datasource, else the panel’s. The panel’s emitted datasource is its own, or the one all its queries share, or Grafana’s -- Mixed -- when they differ. A panel can also be given { type: "datasource", uid: "-- Mixed --" } itself; its queries then each use their own datasource, and one that names none goes to Grafana’s default datasource. A Row’s datasource is written on the row header only; a panel inside the row does not inherit it, as in Grafana, so a panel or query with no datasource uses the dashboard’s default.

Grafana’s grid is 24 columns. A panel with both gridPos.x and gridPos.y is placed exactly there, and its cells are reserved before anything else is placed, wherever it is declared. Any other panel is placed by the dashboard: left to right in declaration order, wrapping to a new line when the next panel would pass column 24, using gridPos.w/h if given and the class’s default size otherwise. It skips cells already taken, so it never lands on an explicitly placed panel. A panel with only x keeps that column and goes on the first line where it is free, after the panel before it; a panel with only y takes the first free column on that line, or on the first line below with room.

A Row starts on the first free line below everything placed so far and takes the full width and one grid unit of height. With gridPos: { y } it goes on that line instead, which is reserved before anything else is placed, as an explicitly placed panel’s cells are. Its panels are placed below it. With collapsed: true they are written inside the row (Grafana’s format for a collapsed row), laid out on their own grid starting under the header, and the next item starts right under the row header.

GRAF105 reports a panel wider than the grid and any two panels that overlap.

Panel and row ids default to 1, 2, 3… in the order they appear, skipping any id set explicitly. Query refIds default to A, B, C… within each panel.

A library panel is kept in Grafana’s library and shared by dashboards: each dashboard holds a reference, { id, gridPos, libraryPanel: { uid, name } }, and Grafana draws the library’s copy. LibraryPanel declares that copy:

import { Dashboard, LibraryPanel, LibraryPanelRef, PromQuery, StatPanel, TimeSeriesPanel } from "@intentius/chant-lexicon-grafana";
export const burnRate = new LibraryPanel({
name: "Burn rate",
uid: "slo-burn-rate",
folder: "SLOs",
panel: new TimeSeriesPanel({ title: "Burn rate", targets: [new PromQuery({ expr: "slo:burn_rate:1h" })] }),
});
export const checkout = new Dashboard({
title: "Checkout",
panels: [
new StatPanel({ title: "Requests" }),
burnRate,
new LibraryPanelRef({ libraryPanel: burnRate, gridPos: { x: 0, y: 8, w: 24, h: 6 }, title: "Burn rate" }),
],
});
LibraryPanel propDefaultNotes
namerequiredthe name the library lists it under; references carry it too
uidthe name as a uid1-40 letters, digits, -, _ (GRAF001)
folderthe folder of the first dashboard that uses ita path or a Folder
panelrequiredany panel; its gridPos and id are left out, since each reference has its own

A LibraryPanel listed in panels (or in a Row’s) is laid out like a panel, at its panel class’s default size. A LibraryPanelRef sets the reference’s gridPos, id and title; Grafana shows the library panel’s own title whatever the reference says. A LibraryPanelRef can also name a library panel the project does not declare, by { uid, name }: one that already exists in Grafana, which the build references and never writes.

The build writes each library panel a dashboard places into that dashboard’s __elements, keyed by uid, as Grafana’s “Export for sharing externally” does, plus the uid of its folder when it names one. GRAF107 checks the model as a panel. What creates the library panel in Grafana depends on how the dashboard gets there:

  • Apply over the API writes it to the library before the dashboards, in its folder.
  • Grafana’s own import (Dashboards → New → Import, or POST /api/dashboards/import) creates it from __elements.
  • GrafanaOperatorResources writes a GrafanaLibraryPanel for it (see Provisioning).
  • File provisioning, and the sidecar ConfigMaps that load the same files, do not: Grafana stores the dashboard with its __elements and draws the reference only once a library panel with that uid exists. Checked against Grafana 12.4.11 and 13.2.2.
ClassGrafana typeKey props
QueryVariablequerydatasource, query, definition, regex, refresh ("never", "onLoad", "onTimeRangeChange"), sort
CustomVariablecustomvalues, each a value or "text : value"
IntervalVariableintervalvalues, auto, autoCount, autoMin
DatasourceVariabledatasourcepluginType, regex
ConstantVariableconstantvalue (always hidden)
TextboxVariabletextboxvalue
AdhocVariableadhocdatasource, filters, baseFilters, defaultKeys, allowCustomValue, enableGroupBy
GroupByVariablegroupbydatasource, options, defaultValue, allowCustomValue
SwitchVariableswitchenabled, enabledValue (default "true"), disabledValue (default "false")

All take name, label, description, hide ("label", "valueOnly", "hidden") and skipUrlSync, and all but the switch take current; the query, custom and datasource variables take multi, includeAll and allValue.

A QueryVariable’s query is a string, or the object the datasource’s variable editor writes, which Grafana hands to the datasource as it is. Prometheus writes { qryType, query, refId }; the PrometheusVariableQuery type lists its fields. The object form keeps the editor’s own form (label values, series query, and so on) when the dashboard is opened in Grafana; definition defaults to the query text.

const namespace = new QueryVariable({
name: "namespace",
datasource: prometheus,
query: { qryType: 1, query: "label_values(kube_namespace_created, namespace)" },
multi: true,
includeAll: true,
});

An AdhocVariable adds its filters ({ key, operator, value }) to every query sent to its datasource, and a GroupByVariable adds a grouping by the keys selected; no query names them. Both ask the datasource for keys unless given defaultKeys or options. A SwitchVariable is $name set to enabledValue or disabledValue.

These three depend on the Grafana version:

ClassGrafana
AdhocVariableevery supported version; enableGroupBy is new in 13 and needs the dashboardUnifiedDrilldownControls feature toggle
GroupByVariablein the dashboard schema since 10.4, but experimental in 12.4 and 13.x: with the groupByVariable feature toggle off, Grafana drops the variable when it loads the dashboard. Grafana 12.4 also cannot export a dashboard that has one
SwitchVariable12.3 and later

A CustomVariable value is written the way Grafana reads it: "Production : prod" shows Production and sets prod, and a comma is part of the value (chant escapes it as \,, and leaves one already escaped alone).

A DatasourceVariable can be passed anywhere a datasource goes. The reference is written as { type: <pluginType>, uid: "${name}" }, and its type parameter keeps a PromQuery from using a Loki datasource variable.

Queries reference variables in their text, $name or ${name}. GRAF103 checks each one is declared on the dashboard; Grafana’s own $__… variables need no declaration.

A panel or row with repeat is copied once per selected value of that variable. Grafana repeats only over a query, custom, datasource or group by variable, and a query, custom or datasource one needs multi or includeAll to have more than one value; GRAF110 warns about a repeat that can only ever show one copy.