| name | superplane-dashboard-and-widgets |
| description | Implements and configures SuperPlane canvas dashboards (markdown, node, table, chart, number panels), widget data sources, CEL/templates, table row trigger actions, and dashboard YAML. Use when editing dashboard UI, panelTypes, useWidgetData, WidgetTable, canvas_dashboard_yml, Get/UpdateCanvasDashboard, or docs/prd/dashboard-and-widgets.md. |
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? |
nodes | Adaptive card: one entry uses the compact single-node layout; multiple entries render as a row list. Optional per-entry Run button (manual-run triggers only). | title?, nodes[] with node, label?, description?, showRun?, triggerName?, promptConfirmation? |
node (legacy) | Same renderer as nodes — the merged card folds legacy single-node content into a one-entry list. Kept for import compatibility; migrates to nodes on first save. | node, showRun?, triggerName? |
table | WidgetTable | dataSource, render.kind: "table" |
chart | WidgetChart (SVG) | dataSource, render.kind: "chart" |
number | WidgetNumber | dataSource, render.kind: "number" |
scorecard | WidgetScorecard — single KPI only (no multi-KPI or composite memory); adds change vs the immediately previous value in the series, direction-aware target/progress, and a status-colored sparkline via the shared Sparkline | dataSource, render.kind: "scorecard" with aggregation, optional field, better, target, showProgress, sparklineField, showChange, changeCaption |
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, trend, …), show, href. format: trend also accepts trendBetter (up/down, default up) and trendDisplay (percent/value/none, default percent); the cell compares against the row directly below in the filtered/sorted table (or the first already-loaded row still hidden by the progressive display window).
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.
Manual-run gate: only the built-in start and schedule triggers expose a user-invokable run hook. The UI filters on node.component against the hardcoded allowlist in web_src/src/pages/app/console/manualRunTriggers.ts — TablePanelForm hides non-manual triggers from the dropdown, WidgetTable hides their row actions, and NodesPanelCard/NodesPanelForm hide the Run affordance. Backend authorization stays in InvokeNodeTriggerHook; adding a new manual-run trigger requires a matching entry in the frontend allowlist.
Expressions
{{ CEL }} — @marcbachmann/cel-js via widget/celExpr.ts; row env + now (Unix seconds). The adapter upfront-coerces safe-integer JS numbers (and, on retry, numeric strings) to BigInt for int arithmetic, and normalizes safe-integer BigInt results back to plain number on the way out.
- 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.
Scorecard shares the number aggregation vocabulary but is single-KPI only (no multi-KPI / composite memory). Comparison model:
- Change = current value vs the immediately previous value in the series. The series is derived from
sparklineField when set, or the primary field as a fallback. Only first / last aggregations expose a natural "previous" (adjacent anchor via pickChangeAnchors); combining aggregations (sum / avg / min / max / count) hide the chip. Reuses computeTrend (widgetTrend.ts) for percent/absolute math.
- Target = literal number or
{{ CEL }} (evaluated against the newest filtered row + now), used for optional showProgress and fallback status color.
better: "up" | "down" controls the polarity for the value change, the sparkline, and the vs-target status.
- The form relabels the two directional aggregations as
Latest / Earliest because all data sources are newest-first (first → Latest, last → Earliest). Persisted YAML still uses first / last.
Helpers live in widget/scorecardMath.ts (extractScorecardSeries, pickChangeAnchors, resolveScorecardTarget, computeScorecardProgress, computeScorecardChange, resolveScorecardStatus, formatScorecardChangeLabel). Rendering is in widget/WidgetScorecard.tsx; the sparkline itself comes from the shared widget/Sparkline.tsx (shared with WidgetNumber) with a className prop for status coloring.
YAML
apiVersion: v1
kind: Dashboard
metadata:
canvasId: <uuid>
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
cd web_src && npm run test:run -- src/pages/workflowv2/dashboard
make format.js
make check.lint.ui
make check.build.ui
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 / Run button | NodesPanelCard.tsx, useConsoleRunTrigger.ts, useConsoleTriggerLock.ts, deriveNodeStatuses.ts |
| Header dashboard actions | dashboardHeaderActions.ts, useDashboardModeActions.ts |
| API hooks | web_src/src/hooks/useCanvasData.ts — useCanvasDashboard, useUpdateCanvasDashboard |