SuperPlane dashboard and widgets
Use this skill when working on per-canvas dashboards: the workflow v2 overlay, typed panels, widget renderers, YAML import/export, or backend validation.
Canonical reference: docs/prd/dashboard-and-widgets.md — read it for full schemas, examples, and maintenance notes. This skill is the operational subset for agents.
Product rules (do not break)
- One dashboard per canvas (not templates). Stored in
canvas_dashboards as JSON panels + layout.
- Dashboard mode hides the graph; 12-column
react-grid-layout (DashboardView).
- Edit (panels, layout, YAML import):
canvases:update, not template, canvas not deleted.
- Run (node panel Run, table row actions): same as edit —
InvokeNodeTriggerHook; UI uses canRunNodes.
- YAML import is replace-all (max 50 panels, 1 MiB payload).
- User-facing name: SuperPlane (capital P).
- Row actions are
kind: trigger only — they fire trigger nodes; they do not call HTTP Request nodes directly.
Layer map
| Layer |
Key paths |
| Host |
web_src/src/pages/workflowv2/index.tsx — mode, feature flag, header |
| Overlay gate |
dashboard/WorkflowDashboardOverlay.tsx |
| Overlay |
dashboard/DashboardOverlay.tsx — query/mutation, context |
| Context |
dashboard/DashboardContext.tsx, DashboardContextProvider.tsx |
| Trigger hook |
dashboard/useDashboardTriggerNode.ts |
| Grid |
dashboard/DashboardView.tsx |
| Local state |
dashboard/useDashboardPanelState.ts (500ms debounced save) |
| Schema |
dashboard/panelTypes.ts — types, templates, validators, normalizeTablePanelContent |
| YAML (FE) |
dashboard/dashboardYaml.ts, DashboardYamlModal.tsx |
| Widget data |
dashboard/widget/useWidgetData.ts |
| Widget UI |
dashboard/widget/WidgetTable.tsx, WidgetChart.tsx, WidgetNumber.tsx |
| Backend |
pkg/models/canvas_dashboard.go, canvas_dashboard_yml.go |
| API |
pkg/grpc/actions/canvases/get_canvas_dashboard.go, update_canvas_dashboard.go |
| Proto |
protos/canvases.proto — DashboardPanel, CanvasDashboard |
Invariant: panelTypes.ts validators, canvas_dashboard_yml.go, and widget types.ts must agree. Frontend fast-fail; backend authoritative on import.
Node references: always accept id or name via resolveDashboardNode in DashboardContext.tsx.
Panel types
type |
Runtime |
Main content |
markdown |
GFM body with {{ name.field }} interpolation |
title?, body?, variables? |
html |
Sanitized HTML body with {{ name.field }} interpolation, scoped <style>, Tailwind via safelist |
title?, body?, variables? |
node |
Status chip + optional Run |
node, showRun?, triggerName? |
table |
WidgetTable |
dataSource, render.kind: "table" |
chart |
WidgetChart (SVG) |
dataSource, render.kind: "chart" |
number |
WidgetNumber |
dataSource, render.kind: "number" |
New panels: templateForPanelType in panelTypes.ts. Draft states (e.g. empty memory namespace) should stay valid where possible.
Data sources (useWidgetData)
{ kind: "memory", namespace: string, fieldPath?: string }
{ kind: "executions", node?: string, limit?: number }
{ kind: "runs", limit?: number }
| Kind |
Query |
Notes |
memory |
useCanvasMemoryEntries |
Filter by namespace; fieldPath flattens nested lists (memoryRow.ts) |
executions |
useInfiniteCanvasEvents |
Flatten executions[]; optional node filter; eager pages until limit or cap (~500 events) |
runs |
useInfiniteCanvasRuns |
totalCount for count KPIs |
Execution rows get status, nodeName, durationMs. Status vocabulary: passed, failed, running, pending, cancelled, unknown.
Table panels (most complex)
Columns
Non-empty field; optional label, format (text, number, status, relative, link, …), show, href.
Filters
render.where[] — AND list; ops: eq, neq, contains, not_contains, gt, lt, exists, not_exists.
Row actions (trigger)
Required: kind: trigger, node (id or name). Optional: hook (default run), template, payload, confirm, show, variant, icon.
Runtime flow: WidgetTable → mergeTriggerPayload → onTriggerNode → useDashboardTriggerNode → InvokeNodeTriggerHook → invalidate events/runs/memory queries.
Legacy fields normalized in FE: target → node, triggerName → template.
Expressions
{{ CEL }} — cel-js via widget/celExpr.ts; row env + now (Unix seconds).
- Legacy
show — e.g. status == "running" (showExpression.ts, rowVisibility.ts).
- Prefer structured
where for simple validated filters.
Lint: loose equality in legacy expressions is intentional (scalar normalization). Do not add eslint-disable for == in dashboard code; refactor instead.
Editor memory hints: MemoryDiscoveryPanel.tsx, useMemoryCatalog.ts (suggestions only; YAML still validated).
Markdown variables
content.variables[] carries named live data refs; body uses {{ name.field }} (or {{ name.$["Node"].data.x }} for runs).
- Sources:
{ kind: "memory", namespace, orderBy?, direction?, matches?, mode?, limit? } (default mode: single first-row wins, orderBy: createdAt desc) or { kind: "run", select: latest | latest_passed | latest_failed }.
mode: list resolves the memory variable to the full sorted array of matching rows (optionally capped by limit), unlocking CEL list macros (rows.map(r, ...).filter(...)) inside {{ }}; pair with the join(list, sep) builtin in celExpr.ts to flatten into Markdown / HTML.
- Resolution lives in
useMarkdownVariables.ts (pickMemoryRows is the exported helper that branches on mode); interpolation in markdownInterpolation.ts (reuses celExpr.compileTemplate/evalTemplate). Validation: markdownVariables.ts (FE, including validateMarkdownContent) + validateMarkdownContent / validateHTMLContent in pkg/models/console_yml.go (BE).
- Run vars expose
status, nodeName, payload, durationMs, and a $ map of node executions (same shape as the table widget).
HTML widget safety
- Render pipeline (
HtmlBody.tsx): interpolate variables → DOMPurify allow-list → scope <style> blocks → dangerouslySetInnerHTML into div[data-console-html-root="<id>"].
- Sanitizer (
htmlSanitize.ts) blocks <script> and all on* handlers, removes head-like and resource-fetching elements (link, meta, base, iframe, object, embed, audio, video, form, svg, math, …), allows <img src>/<img srcset> for http(s)/relative URLs (cross-origin image fetches are permitted by policy), strips poster/background/data/xlink:href, restricts href/src/srcset to http(s)/mailto:/tel:/fragments, and rewrites every <style> rule to scope selectors under the widget root while dropping @import, url(...), and unknown at-rules.
- Tailwind v4 classes must be in the curated
@source inline(...) safelist in web_src/src/App.css to apply at runtime — extend it conservatively, never bypass it.
Chart and number
Chart render.type: bar, stacked-bar, line, area, donut. xField + series[]; omit series[].field to count rows per bucket.
Number aggregations: count, sum, avg, min, max, first, last — non-count requires field.
YAML
apiVersion: v1
kind: Dashboard
metadata:
canvasId: <uuid> # export only; ignored on import
name: <display>
spec:
panels: [{ id, type, content }]
layout: [{ i, x, y, w, h, minW?, minH? }]
- FE:
dashboardYaml.ts — parse/serialize + validatePanelContent
- BE:
DashboardFromYML / DashboardToYML in canvas_dashboard_yml.go
- Unknown fields rejected; missing
panels/layout → empty lists
Agent workflows
Fix a dashboard bug
- Reproduce in dashboard mode (not template); note panel
type and dataSource.kind.
- Trace: panel card →
useWidgetData → widget renderer → (if trigger) useDashboardTriggerNode.
- Check permissions in
pkg/authorization/interceptor.go if RPC-related.
- Add/update test under
web_src/src/pages/workflowv2/dashboard/**/*.spec.ts.
Add or change panel content fields
widget/types.ts (if widget-facing)
panelTypes.ts — interface, templateForPanelType, validatePanelContent, normalization
canvas_dashboard_yml.go — mirror validation
- Panel card + form component
- YAML tests:
dashboardYaml.spec.ts, canvas_dashboard_yml_test.go
Add a new panel type
PANEL_TYPES, PANEL_TYPE_META, validator, template
AllowedDashboardPanelTypes in Go
*PanelCard.tsx + DashboardView PanelCardRouter
- Update docs/prd/dashboard-and-widgets.md
Add a data source kind
- Extend types in
widget/types.ts + panelTypes.ts
DataSourceForm.tsx editor
- Branch in
useWidgetData.ts
- Backend YAML validator + tests
Configure memory table (user/agent task)
Use PRD example; namespace must match canvas memory keys. Row actions target trigger nodes only.
Verification
# Frontend unit tests (dashboard package)
cd web_src && npm run test:run -- src/pages/workflowv2/dashboard
# After UI edits (Docker dev env)
make format.js
make check.lint.ui
make check.build.ui
# After Go validation/API edits
make format.go
make lint
make check.build.app
go test ./pkg/models -run 'TestDashboard|TestValidateDashboardContent'
go test ./pkg/grpc/actions/canvases -run CanvasDashboard
Repo conventions
- No
web_src/src/utils/* — use lib/ or hooks/.
- Dashboard has strict ESLint budget — refactor touched code; do not raise the budget.
- Split large components for Fast Refresh where the codebase already does.
- Never hand-write DB migrations;
make db.migration.create NAME=<dash-name> if persistence changes.
- AGENTS.md: protobuf enum mapping, authorization on new RPCs.
Quick file index
| Task |
Start here |
| Grid / add panel |
DashboardView.tsx, useDashboardPanelState.ts |
| Table CEL / filters / actions |
WidgetTable.tsx, celExpr.ts, evalTableWhere.ts, mergeTriggerPayload.ts |
| Table editor |
TablePanelForm.tsx, TablePanelFormRows.tsx |
| Trigger from dashboard |
useDashboardTriggerNode.ts, dashboardTriggerParameters.ts |
| Node status chip |
NodePanelCard.tsx, deriveNodeStatuses.ts |
| Header dashboard actions |
dashboardHeaderActions.ts, useDashboardModeActions.ts |
| API hooks |
web_src/src/hooks/useCanvasData.ts — useCanvasDashboard, useUpdateCanvasDashboard |
Source: superplanehq/superplane — distributed by TomeVault.
1---2name: superplanehq-superplane-superplane3description: SuperPlane dashboard and widgets4---56# SuperPlane dashboard and widgets78Use this skill when working on **per-canvas dashboards**: the workflow v2 overlay, typed panels, widget renderers, YAML import/export, or backend validation.910**Canonical reference:** [docs/prd/dashboard-and-widgets.md](../../../docs/prd/dashboard-and-widgets.md) — read it for full schemas, examples, and maintenance notes. This skill is the operational subset for agents.1112---1314## Product rules (do not break)1516- One dashboard per **canvas** (not templates). Stored in `canvas_dashboards` as JSON `panels` + `layout`.17- Dashboard mode hides the graph; **12-column** `react-grid-layout` (`DashboardView`).18- **Edit** (panels, layout, YAML import): `canvases:update`, not template, canvas not deleted.19- **Run** (node panel Run, table row actions): same as edit — `InvokeNodeTriggerHook`; UI uses `canRunNodes`.20- YAML import is **replace-all** (max **50** panels, **1 MiB** payload).21- User-facing name: **SuperPlane** (capital P).22- Row actions are **`kind: trigger` only** — they fire trigger nodes; they do not call HTTP Request nodes directly.2324---2526## Layer map2728| Layer | Key paths |29| --- | --- |30| Host | `web_src/src/pages/workflowv2/index.tsx` — mode, feature flag, header |31| Overlay gate | `dashboard/WorkflowDashboardOverlay.tsx` |32| Overlay | `dashboard/DashboardOverlay.tsx` — query/mutation, context |33| Context | `dashboard/DashboardContext.tsx`, `DashboardContextProvider.tsx` |34| Trigger hook | `dashboard/useDashboardTriggerNode.ts` |35| Grid | `dashboard/DashboardView.tsx` |36| Local state | `dashboard/useDashboardPanelState.ts` (500ms debounced save) |37| Schema | `dashboard/panelTypes.ts` — types, templates, validators, `normalizeTablePanelContent` |38| YAML (FE) | `dashboard/dashboardYaml.ts`, `DashboardYamlModal.tsx` |39| Widget data | `dashboard/widget/useWidgetData.ts` |40| Widget UI | `dashboard/widget/WidgetTable.tsx`, `WidgetChart.tsx`, `WidgetNumber.tsx` |41| Backend | `pkg/models/canvas_dashboard.go`, `canvas_dashboard_yml.go` |42| API | `pkg/grpc/actions/canvases/get_canvas_dashboard.go`, `update_canvas_dashboard.go` |43| Proto | `protos/canvases.proto` — `DashboardPanel`, `CanvasDashboard` |4445**Invariant:** `panelTypes.ts` validators, `canvas_dashboard_yml.go`, and widget `types.ts` must agree. Frontend fast-fail; backend authoritative on import.4647**Node references:** always accept **id or name** via `resolveDashboardNode` in `DashboardContext.tsx`.4849---5051## Panel types5253| `type` | Runtime | Main `content` |54| --- | --- | --- |55| `markdown` | GFM body with `{{ name.field }}` interpolation | `title?`, `body?`, `variables?` |56| `html` | Sanitized HTML body with `{{ name.field }}` interpolation, scoped `<style>`, Tailwind via safelist | `title?`, `body?`, `variables?` |57| `node` | Status chip + optional Run | `node`, `showRun?`, `triggerName?` |58| `table` | `WidgetTable` | `dataSource`, `render.kind: "table"` |59| `chart` | `WidgetChart` (SVG) | `dataSource`, `render.kind: "chart"` |60| `number` | `WidgetNumber` | `dataSource`, `render.kind: "number"` |6162New panels: `templateForPanelType` in `panelTypes.ts`. Draft states (e.g. empty memory namespace) should stay valid where possible.6364---6566## Data sources (`useWidgetData`)6768```ts69{ kind: "memory", namespace: string, fieldPath?: string }70{ kind: "executions", node?: string, limit?: number }71{ kind: "runs", limit?: number }72```7374| Kind | Query | Notes |75| --- | --- | --- |76| `memory` | `useCanvasMemoryEntries` | Filter by namespace; `fieldPath` flattens nested lists (`memoryRow.ts`) |77| `executions` | `useInfiniteCanvasEvents` | Flatten `executions[]`; optional node filter; eager pages until `limit` or cap (~500 events) |78| `runs` | `useInfiniteCanvasRuns` | `totalCount` for count KPIs |7980Execution rows get `status`, `nodeName`, `durationMs`. Status vocabulary: `passed`, `failed`, `running`, `pending`, `cancelled`, `unknown`.8182---8384## Table panels (most complex)8586### Columns8788Non-empty `field`; optional `label`, `format` (`text`, `number`, `status`, `relative`, `link`, …), `show`, `href`.8990### Filters9192`render.where[]` — AND list; ops: `eq`, `neq`, `contains`, `not_contains`, `gt`, `lt`, `exists`, `not_exists`.9394### Row actions (trigger)9596Required: `kind: trigger`, `node` (id or name). Optional: `hook` (default `run`), `template`, `payload`, `confirm`, `show`, `variant`, `icon`.9798Runtime flow: `WidgetTable` → `mergeTriggerPayload` → `onTriggerNode` → `useDashboardTriggerNode` → `InvokeNodeTriggerHook` → invalidate events/runs/memory queries.99100Legacy fields normalized in FE: `target` → `node`, `triggerName` → `template`.101102### Expressions103104- **`{{ CEL }}`** — `cel-js` via `widget/celExpr.ts`; row env + `now` (Unix seconds).105- **Legacy** `show` — e.g. `status == "running"` (`showExpression.ts`, `rowVisibility.ts`).106- Prefer structured `where` for simple validated filters.107108**Lint:** loose equality in legacy expressions is intentional (scalar normalization). Do not add `eslint-disable` for `==` in dashboard code; refactor instead.109110Editor memory hints: `MemoryDiscoveryPanel.tsx`, `useMemoryCatalog.ts` (suggestions only; YAML still validated).111112### Markdown variables113114- `content.variables[]` carries named live data refs; body uses `{{ name.field }}` (or `{{ name.$["Node"].data.x }}` for runs).115- Sources: `{ kind: "memory", namespace, orderBy?, direction?, matches?, mode?, limit? }` (default `mode: single` first-row wins, `orderBy: createdAt desc`) or `{ kind: "run", select: latest | latest_passed | latest_failed }`.116- `mode: list` resolves the memory variable to the full sorted array of matching rows (optionally capped by `limit`), unlocking CEL list macros (`rows.map(r, ...).filter(...)`) inside `{{ }}`; pair with the `join(list, sep)` builtin in `celExpr.ts` to flatten into Markdown / HTML.117- Resolution lives in `useMarkdownVariables.ts` (`pickMemoryRows` is the exported helper that branches on mode); interpolation in `markdownInterpolation.ts` (reuses `celExpr.compileTemplate`/`evalTemplate`). Validation: `markdownVariables.ts` (FE, including `validateMarkdownContent`) + `validateMarkdownContent` / `validateHTMLContent` in `pkg/models/console_yml.go` (BE).118- Run vars expose `status`, `nodeName`, `payload`, `durationMs`, and a `$` map of node executions (same shape as the table widget).119120### HTML widget safety121122- Render pipeline (`HtmlBody.tsx`): interpolate variables → DOMPurify allow-list → scope `<style>` blocks → `dangerouslySetInnerHTML` into `div[data-console-html-root="<id>"]`.123- Sanitizer (`htmlSanitize.ts`) blocks `<script>` and all `on*` handlers, removes head-like and resource-fetching elements (`link`, `meta`, `base`, `iframe`, `object`, `embed`, `audio`, `video`, `form`, `svg`, `math`, …), allows `<img src>`/`<img srcset>` for `http(s)`/relative URLs (cross-origin image fetches are permitted by policy), strips `poster`/`background`/`data`/`xlink:href`, restricts `href`/`src`/`srcset` to `http(s)`/`mailto:`/`tel:`/fragments, and rewrites every `<style>` rule to scope selectors under the widget root while dropping `@import`, `url(...)`, and unknown at-rules.124- Tailwind v4 classes must be in the curated `@source inline(...)` safelist in `web_src/src/App.css` to apply at runtime — extend it conservatively, never bypass it.125126---127128## Chart and number129130**Chart** `render.type`: `bar`, `stacked-bar`, `line`, `area`, `donut`. `xField` + `series[]`; omit `series[].field` to count rows per bucket.131132**Number** aggregations: `count`, `sum`, `avg`, `min`, `max`, `first`, `last` — non-`count` requires `field`.133134---135136## YAML137138```yaml139apiVersion: v1140kind: Dashboard141metadata:142 canvasId: <uuid> # export only; ignored on import143 name: <display>144spec:145 panels: [{ id, type, content }]146 layout: [{ i, x, y, w, h, minW?, minH? }]147```148149- FE: `dashboardYaml.ts` — parse/serialize + `validatePanelContent`150- BE: `DashboardFromYML` / `DashboardToYML` in `canvas_dashboard_yml.go`151- Unknown fields rejected; missing `panels`/`layout` → empty lists152153---154155## Agent workflows156157### Fix a dashboard bug1581591. Reproduce in dashboard mode (not template); note panel `type` and `dataSource.kind`.1602. Trace: panel card → `useWidgetData` → widget renderer → (if trigger) `useDashboardTriggerNode`.1613. Check permissions in `pkg/authorization/interceptor.go` if RPC-related.1624. Add/update test under `web_src/src/pages/workflowv2/dashboard/**/*.spec.ts`.163164### Add or change panel `content` fields1651661. `widget/types.ts` (if widget-facing)1672. `panelTypes.ts` — interface, `templateForPanelType`, `validatePanelContent`, normalization1683. `canvas_dashboard_yml.go` — mirror validation1694. Panel card + form component1705. YAML tests: `dashboardYaml.spec.ts`, `canvas_dashboard_yml_test.go`171172### Add a new panel type1731741. `PANEL_TYPES`, `PANEL_TYPE_META`, validator, template1752. `AllowedDashboardPanelTypes` in Go1763. `*PanelCard.tsx` + `DashboardView` `PanelCardRouter`1774. Update [docs/prd/dashboard-and-widgets.md](../../../docs/prd/dashboard-and-widgets.md)178179### Add a data source kind1801811. Extend types in `widget/types.ts` + `panelTypes.ts`1822. `DataSourceForm.tsx` editor1833. Branch in `useWidgetData.ts`1844. Backend YAML validator + tests185186### Configure memory table (user/agent task)187188Use PRD example; namespace must match canvas memory keys. Row actions target **trigger nodes** only.189190---191192## Verification193194```bash195# Frontend unit tests (dashboard package)196cd web_src && npm run test:run -- src/pages/workflowv2/dashboard197198# After UI edits (Docker dev env)199make format.js200make check.lint.ui201make check.build.ui202203# After Go validation/API edits204make format.go205make lint206make check.build.app207go test ./pkg/models -run 'TestDashboard|TestValidateDashboardContent'208go test ./pkg/grpc/actions/canvases -run CanvasDashboard209```210211---212213## Repo conventions214215- No `web_src/src/utils/*` — use `lib/` or `hooks/`.216- Dashboard has **strict ESLint budget** — refactor touched code; do not raise the budget.217- Split large components for Fast Refresh where the codebase already does.218- **Never** hand-write DB migrations; `make db.migration.create NAME=<dash-name>` if persistence changes.219- AGENTS.md: protobuf enum mapping, authorization on new RPCs.220221---222223## Quick file index224225| Task | Start here |226| --- | --- |227| Grid / add panel | `DashboardView.tsx`, `useDashboardPanelState.ts` |228| Table CEL / filters / actions | `WidgetTable.tsx`, `celExpr.ts`, `evalTableWhere.ts`, `mergeTriggerPayload.ts` |229| Table editor | `TablePanelForm.tsx`, `TablePanelFormRows.tsx` |230| Trigger from dashboard | `useDashboardTriggerNode.ts`, `dashboardTriggerParameters.ts` |231| Node status chip | `NodePanelCard.tsx`, `deriveNodeStatuses.ts` |232| Header dashboard actions | `dashboardHeaderActions.ts`, `useDashboardModeActions.ts` |233| API hooks | `web_src/src/hooks/useCanvasData.ts` — `useCanvasDashboard`, `useUpdateCanvasDashboard` |234235---236> Source: [superplanehq/superplane](https://github.com/superplanehq/superplane) — distributed by [TomeVault](https://tomevault.io).237<!-- tomevault:4.0:skill_md:2026-06-27 -->