How to build and operate dashboards. For the design and architecture, see DESIGN.md; for the canonical list of what exists, see DESIGN.md §13.
- Dashboards and panels
- Variables
- Repeating panels
- Data links (drill-down)
- Time range and refresh
- GitOps and history
- The Prometheus connection
- Authentication
- Importing from Grafana
Every dashboard is a file (.yaml / .json / .toml) in the dashboards
directory. You edit it in the UI and it stays a git-friendly file — a
one-field edit produces a one-line diff.
Create one with New dashboard (top right of the Dashboards page):
give it a title and a file name (auto-slugified; one / allowed to put it in
a folder), and either start blank or import from Grafana (see below).
On an empty instance the home screen also offers one-click example
dashboards ("Prometheus overview", "Prometheus internals") built from
metrics Prometheus exposes about itself, so they show data on any install.
Two editors for an existing dashboard:
- Visual editor (
/d/:name/edit) — a 24-column grid:- Move/resize a panel by dragging its header or its resize corner.
- Sections group panels; add one with Add section and give it an optional title.
- Add panel (per section) drops in a Time series panel and opens its editor; Edit and ✕ appear on hover over a panel.
- Undo/Redo (
⌘Z/⇧⌘Z), Save (⌘S, only when there are changes) or Discard. The grid below the toolbar is a live preview with the real variables and time range.
- As-code editor (
/d/:name/code) — the raw file in your chosen format, validated on save (schema errors are shown inline). Anything the visual editor can't express yet you can always write here.
The panel editor (the drawer in the visual editor) has: Title,
Description (shown on the dashboard as an ⓘ marker in the panel header,
with the text as its tooltip; variables are interpolated in it), Type;
the type-specific options (below); Queries
(a PromQL expression plus a Legend format like {{instance}} — {{mountpoint}}); Links; and
Repeat.
Common options: Unit (short, percent, bytes, bytes/s, seconds, …) and, on
value panels, Thresholds (ordered value → status of warning / serious
/ critical; below the first threshold is "good").
| Type | Use for | Key options |
|---|---|---|
| Time series | metrics over time | unit; legend bottom/right/hidden; stack; fill area; Y min/max |
| Stat | one big number + sparkline | unit; calculation last/mean/min/max/sum; decimals; sparkline; thresholds |
| Gauge | a value within a range | unit; min (0) / max (100); thresholds (color the arc) |
| Table | series as rows | unit; thresholds (cell color + in-cell bar) |
| Bar | compare series | unit; calculation; orientation; sort |
| Heatmap | histogram (le buckets) over time |
bucket unit (de-cumulates Prometheus buckets) |
| Status grid | many entities' health as a honeycomb | value mapping (Nagios 0=OK / boolean up-down / custom); reduce (last/min/max/mean); name label; tile order (worst-first/query); tiles per row; max tiles |
| Text (Markdown) | notes/headings | Markdown content (raw HTML not rendered) |
| Image | a static image | URL; alt text; fit (contain default / cover / fill) |
| Clock | wall-clock (great in kiosk mode) | format (time / datetime / date / long date / ISO); UTC; font size |
Variables become toolbar selectors and are interpolated into your queries. Edit them from the Variables… button in the editor. Two kinds:
- Label values — options come from Prometheus label values. Fields:
Name (the
$nameidentifier), Label (toolbar display name), Label name (the Prometheus label to list, e.g.instance), Default value, Allow "All", Allow multiple, and Matchers (one series selector per line to narrow the values). - Text — a fixed value; tick Constant to hide it from the toolbar (handy for a value referenced in queries but not meant to be changed).
Reference a variable in PromQL as $name or ${name}:
rate(node_cpu_seconds_total{instance=~"$instance"}[5m])
- "All" (when Allow "All" is on) resolves to the regex
.+, so use it with a regex matcher (=~). - Multiple values render as a regex alternation (
a|b|c, each escaped), again for=~. A single value is inserted verbatim (safe for=).
Chaining — a variable's Matchers can reference another variable, so its options depend on the current selection:
node_uname_info{instance=~"$instance"} # this variable's values follow $instance
Selections are mirrored into the URL as var-<name>=<value> (repeated per
value for multi-select), so a dashboard link is shareable with its filters.
Repeat renders one panel definition once per value of a variable — define
a panel once, get a tile per instance, and the layout grows on its own as the
underlying set changes. The Per-node CPU row in the screenshot at the top of
this guide is a single panel repeated across $instance.
Configure it in the panel editor under Repeat → Repeat by variable (pick the variable; optionally choose a direction), or in the file:
panels:
cpu:
kind: Panel
spec:
display:
name: CPU — $instance # the title interpolates per instance
repeat:
variable: instance # repeat over this variable
direction: horizontal # optional: horizontal (default) | vertical
# maxPerRow: 3 # optional cap on tiles per row
plugin:
kind: TimeSeriesChart
queries:
- kind: TimeSeriesQuery
spec:
plugin:
kind: PrometheusTimeSeriesQuery
spec:
query: rate(node_cpu_seconds_total{instance="$instance"}[5m])Behavior:
- It repeats over the variable's current selection, or over all of its options when "All" is selected.
- Each instance pins the variable to its own value, so every tile runs its
own interpolated query (
instance="web-1",instance="web-2", …). - Put the variable in the panel title (
CPU — $instance) so each tile labels itself. - Horizontal tiles wrap to new rows and push the panels below them down;
verticalstacks them in one column.
Keep a repeated panel alone on its grid row — that is what the editor produces and what the reflow is tuned for.
Repeating a whole row/section (rather than a single panel) is planned.
Data links turn a panel into a jumping-off point: they appear in the panel header and carry the current context — dashboard variables and the selected time range — into the target URL. The Runbook, Node detail and Metrics links in the screenshot's panel headers are data links.
Configure them in the panel editor under Links (title + URL, and whether to open in a new tab), or in the file:
panels:
network:
kind: Panel
spec:
display:
name: Network throughput
links:
# Internal drill-down: another dashboard, context carried over.
- title: Node detail
url: /d/node-detail?var-instance=$instance&from=$__from&to=$__to
# External tool, opened in a new tab.
- title: Runbook
url: https://wiki.example.com/runbook/$job
targetBlank: true
plugin:
kind: TimeSeriesChartWhat gets interpolated into the URL:
- Variables —
$name/${name}, percent-encoded (multi-value joined with commas). - Time range —
$__fromand$__to, epoch milliseconds of the current range.
Use relative URLs (/d/…) to drill into another dashboard with the same
filters applied (the receiving dashboard restores them from the var-…
params), and absolute URLs for external tools.
Internal links currently trigger a full page load; an in-app navigation path is a planned refinement.
The dashboard toolbar controls the window every panel queries:
- Time range — presets Last 5m / 15m / 1h / 6h / 24h / 7d, or Custom… for an absolute From/To range.
- Auto-refresh — off / 10s / 30s / 1m / 5m — and a Refresh now button.
- Defaults come from the dashboard file (
spec.duration,spec.refreshInterval); the current range is not written back — it lives in the URL (?from=now-1h, or?from=<ms>&to=<ms>for absolute), so a link is shareable.
Zoom by dragging a rectangle across any time-series chart (sets an absolute range); double-click a chart to zoom out ×2; re-pick a preset to reset.
Fullscreen a panel by clicking its title ("View fullscreen"); it opens in
place (?viewPanel=<key>), with Esc or Back to dashboard to leave.
Kiosk mode (toolbar button, or ?kiosk=1) hides the nav, banners, and the
whole toolbar for wall displays — press Esc to exit. Pair it with a Clock
panel and auto-refresh for a NOC screen.
When the dashboards directory is a git repository, Dashglass reads its history
straight from git (it needs the git binary on PATH and the directory inside
a git work tree; otherwise the History features are hidden).
- History drawer (toolbar → History) lists the commits touching this dashboard (short hash, subject, author, relative time); expand one to see a diff against the current file. Restore this version writes the old content back as a normal save — uncommitted, so you review and commit it in git (or let commit-on-save do it).
- Commit-on-save (
git.commit_on_saveindashglass.yaml) commits every UI save, authored by the signed-in user sogit blamestays honest. - Remote sync pulls on an interval and pushes right after a commit; on a
diverged history it rebases, and if that conflicts it stops and asks you to
resolve it in the repo. Configure it in
dashglass.yamlundergit.remote(or from Admin → Git sync); credentials (HTTPS token or SSH deploy key) are stored server-side and never put on a command line. - Provisioned / read-only (
-readonly, ordashboards.readonly: true) makes the API refuse writes and the UI drop every edit affordance — the dashboards are managed entirely by editing the files in git. Even admins cannot save in this mode.
The browser never talks to Prometheus: every query goes through the
backend's read-only proxy, and the connection settings (including all
credentials) stay server-side. Point Dashglass at Prometheus with
-prometheus-url / DASHGLASS_PROMETHEUS_URL / prometheus.url in
dashglass.yaml — provisioning it this way locks the admin UI — or
configure it at runtime from Admin → Datasource.
Authentication towards Prometheus: basic auth (native Prometheus
web.config.yml, or a reverse proxy in front) or a bearer token.
Beyond credentials:
- Extra headers are sent with every upstream request — the
Mimir/Cortex tenancy header (
X-Scope-OrgID: tenant-1), or a customAuthorizationscheme with the auth mode left on "none". - CA certificate (PEM) verifies a self-signed or private-CA Prometheus endpoint properly (it replaces the system trust store for this connection).
- Client certificate + key (PEM) enable mTLS, Prometheus's
native
client_auth_type: RequireAndVerifyClientCert. - Skip TLS verification is the last resort when the certificate cannot be fixed — anyone on the network path can then impersonate the datasource, so prefer the CA field.
In dashglass.yaml the TLS material is provisioned as file paths
(prometheus.ca_file, client_cert_file, client_key_file,
insecure_skip_verify, plus headers) — see
dashglass.example.yaml. Configured from the
admin UI, certificates are pasted as PEM and persisted server-side
(settings.yaml, mode 0600); the client key is write-only and never
shown again. Test connection probes the full stack — TLS, headers
and credentials — before saving.
A fresh instance is open: until the first user exists, everyone is an anonymous admin so you can set things up. A banner reminds you of this.
Enable auth by creating the first user in Admin → Users → Create the first user (it must be an admin). From then on everyone signs in.
Roles are cumulative:
| Role | Can |
|---|---|
| viewer | view dashboards, query Prometheus, browse git history |
| editor | + create / edit / delete dashboards, import from grafana.com |
| admin | + everything under Admin: users, datasource, git sync |
The last admin cannot be deleted or demoted; passwords are at least 8 characters. (Read-only mode overrides roles — nobody can write.)
Automated / break-glass:
DASHGLASS_BOOTSTRAP_ADMIN_PASSWORD=…creates anadminuser at startup if none exists.dashglass user add <name> [role](alsopasswd,rm) manages local users from the CLI (role defaults to admin).
Identity providers are configured in dashglass.yaml under auth:
(annotated reference: dashglass.example.yaml).
${ENV} references expand from the environment, so secrets stay out of the
file; precedence is flags > environment > file > defaults, and anything
provisioned by the deployment shows read-only in the admin UI. Every provider
shares one group → role rule (role_mapping): the highest match wins,
* matches any authenticated user, and no match means access is denied —
a shared IdP grants nothing until a group is listed.
Add the block below to dashglass.yaml and restart. The default local login
stays available only until you enable a provider that replaces it.
Native OIDC (Authentik, Keycloak, Pocket ID, Auth0, Okta…). Dashglass runs the authorization-code flow itself — no reverse proxy required.
-
In your IdP, register an application (confidential client, authorization-code + PKCE) and set its redirect URI to
https://<your-dashglass-host>/api/auth/oidc/callback. -
Collect the issuer URL, client ID and client secret, and make sure the ID token carries a groups claim.
-
Configure it (put the secret in the environment):
auth: oidc: issuer: https://auth.example.com/application/o/dashglass/ client_id: dashglass client_secret: ${OIDC_CLIENT_SECRET} redirect_url: https://dashglass.example.com/api/auth/oidc/callback groups_claim: groups # optional (default "groups") role_mapping: admin: [dashglass-admins] editor: [dev-team] viewer: ["*"] # any authenticated user can view
-
Set
OIDC_CLIENT_SECRETand restart. The login page now offers your provider; each user's role is derived from their groups.
Default scopes are openid profile email groups; the username is taken from
preferred_username, then email, then sub. A user whose groups match no
rule is denied (never silently defaulted to viewer).
If the provider's certificate cannot be verified (self-signed, or missing
SANs — Go rejects certificates that only carry the legacy Common Name),
insecure_skip_verify: true disables TLS verification towards the provider.
This lets anyone on the network path impersonate the issuer, so treat it as
a stopgap and prefer fixing the certificate.
If an authenticating proxy already fronts Dashglass (Authelia, Authentik,
oauth2-proxy, Tailscale…), let it be the identity source — setting
user_header disables local login.
auth:
proxy:
user_header: Remote-User
groups_header: Remote-Groups # optional
trusted_proxies: ["10.0.0.0/8"] # only honor headers from these peers
role_mapping:
admin: [dashglass-admins]
editor: [dev-team]
viewer: ["*"]Set trusted_proxies to the proxy's address range. Leave it unset only
if the backend is unreachable except through the proxy — otherwise anyone who
can reach the backend directly could spoof the identity headers.
Not available yet — LDAP is planned (DESIGN.md §13). In the meantime, put a reverse proxy in front or use OIDC (most directories expose an OIDC bridge).
In the New dashboard dialog, open ▸ Import from Grafana:
- From grafana.com — paste a dashboard URL or its numeric id and press
Load (available unless the instance runs with
-disable-grafana-com; the backend only accepts a numeric id and only talks tografana.com, to rule out SSRF). - JSON export — paste or upload the JSON from Grafana's Share → Export.
Conversion runs in the browser and is best-effort. It maps the common
panels (time series/graph, stat/singlestat, gauge, table, bar gauge/bar
chart, heatmap, text), turns Grafana rows into sections, converts PromQL
targets (rewriting [[var]]/${var:fmt} templating and legendFormat), and
migrates label_values(...) and constant/textbox variables.
Anything it can't map is imported as-is (never dropped): an unmapped panel keeps its Grafana type and renders as an "unknown panel" you can fix later, and every approximation is listed under "Imported with approximations:" so there are no silent surprises. Review the result in the editor before saving.
