Dashboard Widgets
Four widget types render on per-layer dashboards. The renderer (apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue) branches on widget.type and delegates to a small component per kind.
Grid context
- 12-column grid (
grid-template-columns: repeat(12, minmax(0, 1fr))). - Row height 120 px (
grid-auto-rows). - Gap 10 px.
grid-auto-flow: dense— gaps backfill with smaller widgets.spandefaults to 4 (three widgets per row);rowSpandefaults to 1.- Legacy 24-col coordinates:
whalves to 12-col,h / 8becomes row count. Old templates keep working. - Responsive collapse below 1100 px viewport.
Common widget shape
interface DashboardWidget {
id: string;
title: string;
tip?: string;
type: 'card' | 'line' | 'top' | 'record';
expressions: string[];
expressionLabels?: string[];
expressionUnits?: string[];
expressionAxes?: number[];
unit?: string;
format?: 'int' | 'decimal' | 'compact';
span?: number;
rowSpan?: number;
visibleWhen?: string;
layerScope?: boolean;
}
| Field | Notes |
|---|---|
expressions[] |
MQE expressions. card typically uses one; line one-per-series; top one-per-tab. |
expressionLabels[] |
Used by top for tab labels and by line for legend names. |
expressionUnits[] |
Per-expression unit override (mixed-unit charts). |
expressionAxes[] |
0 = left axis (default), 1 = right axis. |
unit |
Widget-level default. |
format |
int, decimal, compact. |
visibleWhen |
Predicate. #entity.<key> (hides the widget unless the named entity is selected) or <metric> has value (hides unless the metric returns data). |
layerScope |
Evaluate against the whole layer rather than the selected service. |
card
Renders: Single scalar value with optional unit, formatted per format.
When to use
The widget’s MQE collapses to a single number. Detect by looking at the outermost MQE call: latest(...), max(...), min(...), avg(<plain-metric>), sum(<plain-metric>) are scalar-collapse functions.
A line widget with a scalar-shaped MQE renders a one-point chart, which is misleading. Use card.
Example
{
"id": "error_rate",
"title": "Error rate",
"type": "card",
"expressions": ["service_sla/100"],
"unit": "%",
"format": "decimal",
"span": 3
}
line
Renders: Multi-series line chart via the TimeChart component (ECharts wrapper).
Multi-series
One series per expression in expressions[]. Labels from expressionLabels[] populate the legend.
{
"id": "latency",
"title": "Latency percentiles",
"type": "line",
"expressions": [
"service_percentile{p='50'}",
"service_percentile{p='95'}",
"service_percentile{p='99'}"
],
"expressionLabels": ["P50", "P95", "P99"],
"unit": "ms",
"span": 6,
"rowSpan": 2
}
Dual y-axis
When any series has yAxisIndex: 1, the right axis appears. Use for mixed-unit charts where one series is throughput (rpm) and another is latency (ms).
{
"id": "traffic_vs_latency",
"title": "Traffic vs P95",
"type": "line",
"expressions": ["service_cpm", "service_percentile{p='95'}"],
"expressionLabels": ["Throughput", "P95"],
"expressionUnits": ["rpm", "ms"],
"expressionAxes": [0, 1],
"span": 6,
"rowSpan": 2
}
Behavior
- Smooth lines with circle markers.
- Legend visible when more than one series; hidden for single series.
- Tooltip positioned via callback (appendToBody) so it does not clip near grid edges.
- Synced crosshairs: pointing at a time on this chart highlights the same time on every other
linechart on the page. - Fingerprinting: data-only updates (same structure, new values) animate smoothly. Structure changes do a full replace.
When line is wrong
- MQE collapses to one number → use
card. - MQE returns a sorted list of (label, value) → use
top.
top
Renders: Sorted list. Rank + name + value with a background fill bar normalized to the maximum.
Tabs
When expressions[] has multiple entries, a tab switcher above the list lets the operator flip between expressions (each tab is a separate sort). Labels from expressionLabels[]; units from expressionUnits[].
Example
{
"id": "top_apis",
"title": "Top 20 APIs",
"type": "top",
"expressions": [
"top_n(endpoint_cpm, 20, des)",
"top_n(endpoint_resp_time, 20, des)",
"top_n(endpoint_sla, 20, asc)"
],
"expressionLabels": ["Traffic", "Slow", "Errors"],
"expressionUnits": ["rpm", "ms", "%"],
"span": 3,
"rowSpan": 4
}
Behavior
- Rows are clickable when the result includes an entity reference — typically navigates to the per-endpoint or per-instance drill-down.
- Bar fill normalized per-tab (each tab has its own max).
- Background color follows the layer accent.
MQE requirements
The MQE must return a labeled list. top_n(<metric>, N, <des|asc>) is the canonical shape. aggregate_labels(...) can also produce list-shaped output.
record
Renders: Tabular records. Used for “slow SQL”, “slow statements”, and similar list-of-records output.
When to use
The data source returns a record set (rows × typed columns) rather than a numeric time series. Examples:
- Slow SQL statements with execution time, count, statement text.
- Slow gRPC calls with method name, latency, status code.
Example
{
"id": "slow_sql",
"title": "Slow SQL",
"type": "record",
"expressions": ["top_n(database_slow_statement, 20, des)"],
"span": 6,
"rowSpan": 4
}
Behavior
- Renders as a dense table with column headers from the record’s typed fields.
- Sort, filter, pagination handled in the component.
Visibility predicates
visibleWhen lets a widget hide itself based on context:
#entity.serviceInstance— only show when an instance is selected. Useful for “instance details” widgets on the service page that should not render at the service-only level.#entity.endpoint— only show when an endpoint is selected.<metric-name> has value— only show when the named metric returns non-null data. Useful for layer-conditional widgets (e.g. JVM metrics only on JVM-based services).
The predicate is evaluated on every data refresh; the widget disappears (rather than rendering empty) when the predicate is false.
Layer scope
layerScope: true runs the MQE against the layer rather than the currently selected service. Useful for layer-wide summaries on the service page (e.g., “this service” + “all services in this layer” side by side).
{
"id": "layer_total_rpm",
"title": "Layer total RPM",
"type": "card",
"expressions": ["sum(service_cpm)"],
"layerScope": true,
"unit": "rpm",
"span": 3
}
Choosing the right widget
| MQE outermost call | Widget type |
|---|---|
latest(...), max(...), min(...), avg(<plain>), sum(<plain>) |
card |
rate(...), increase(...), relabels(...), aggregate_labels(...) without scalar collapse, histogram*(...) |
line |
top_n(...) returning labeled list |
top |
| Record-shaped output (slow SQL, slow gRPC) | record |
The widget editor (planned) will warn on type / MQE mismatches. The schema does not enforce — author carefully.
Per-scope widget sets
The dashboards.<scope> map on a layer template lets you define different widget grids for service / instance / endpoint / topology / trace / logs / profiling pages. Scope resolution falls back to service if a specific scope is unset (apps/bff/src/logic/layers/loader.ts:widgetsForScope()).
See Customization → Layer Dashboard Templates for the per-scope structure.