feat(dashboards): make the widget builder a widget-type framework - #282
Conversation
Adding a widget type used to mean ~35 edits across 4 packages. The type was a
string smeared over four if-chains in widget-builder-utils.ts, nine boolean
flags in the settings rail, nine more in the picker, four forked copies of the
default grid size, and a Record<string, Component> that fell back to a line
chart on a typo. `VisualizationType` ended in `(string & {})`, so TypeScript
gave no exhaustiveness checking anywhere.
Now a widget type is one file plus one registry line, enforced by the compiler.
Two layers:
- packages/domain/src/http/widget-types.ts — the type-agnostic table (label,
visualization, canonical chartId, default layout, template height, raw-SQL
display type, owned display keys, group-by requirement, MCP exposure, Perses
kinds). The API, MCP tools, Perses importer, web store and portable-dashboard
importer all read it.
- apps/web/.../widgets/types/ — Record<PanelType, WidgetTypeDefinition>,
exhaustive: a new PanelType is a compile error until it has a renderer, icon,
settings panel, preset preview and the three lowering functions.
Composition cleanups: the settings rail's 7 `isX` + 2 `showX` booleans become
per-type ConfigPanels composed from a WidgetSettings.* namespace reading state
from context; the five near-identical chart renderers collapse into one factory;
the five dead action props come off all ten renderers (WidgetShell already
resolved them from WidgetActionsContext).
widget-builder-utils 864 -> 288, widget-settings-bar 592 -> 35,
chart-picker 650 -> 222. Net -2,063 lines.
Fixes found on the way: KNOWN_VISUALIZATIONS was missing `markdown`; the chat
approval renderer knew 4 of 10 kinds; the template-preview `kind` literal knew 6
of 12, so a gauge reported itself as a table; portable-dashboard used minW 3
where the store used 2; and `thresholds` set on a pie did nothing because
pie-widget.tsx never forwarded them.
No persisted shape change and no migration — `visualization` stays a string on
the wire.
Also bundles the in-flight refreshIntervalSeconds work that was already in the
tree; it shares five files with this change and could not be split out. Its two
outstanding failures are fixed here: the missing `refresh_interval_changed`
history label, and the v2 contract test's dashboard fixture.
|
Your Pullfrog Router balance is exhausted. You have a payment method on file but auto-reload is disabled, so runs paused once your balance went past the overdraft buffer. Top up balance → · Enable auto-reload →
|
🍁 Maple PR previewNote Preview resources were removed when this pull request closed. Final commit |
Three separate causes, only the first from this branch.
1. TypeScript (`turbo typecheck test build`) — a real regression from the
widget-type table. `visualizationToDisplayType("list")` returned `line`
because the old switch let `list` fall through to `default`; giving the
domain entry `rawSqlDisplayType: "table"` silently changed what an
MCP-created `visualization: "list"` + `sql` widget renders as. A list is
configured by ListConfigPanel and never by SQL — the editor hides the Raw SQL
toggle for it — so it has no raw-SQL rendering and falls back to `line` like
markdown. Pinned in widget-types.test.ts.
Found only because CI runs the whole suite: the scoped run used while
building this branch never loaded raw-sql-widget.test.ts.
2. Knip — `rules.files` is the only error-level rule, and
apps/web/src/components/infra/pod-honeycomb.tsx has been unreferenced since
#269 moved the infra page off it. Deleted, and dropped the stale
`**/sst-env.d.ts` ignore knip flagged as unused config. The remaining
findings are all `warn` and do not fail the check.
3. Local checkpoint/archive native — red on main since 977989e, both dying at
the same place: `maple stop` gave the server 5s to exit, but a clean shutdown
flushes telemetry under its own 3s bound (`shutdownTimeout` in
core/telemetry.ts) and then closes chDB. That measures ~3.5s on a warm
laptop, so the cap left under two seconds of headroom and a loaded 4-vCPU
runner blew through it — the smoke tests were failing on servers that were
shutting down entirely correctly. Budget raised to 15s, dots throttled to one
per half-second so the longer wait doesn't print a wall of them.
Verified by running both native smokes locally against a freshly built
bundle: checkpoint passes all reopen cycles, archive passes with exact DuckDB
counts, plus the crash-recovery, adversarial and merge probes that CI had
been skipping because the lifecycle step failed first.
|
Your Pullfrog Router balance is exhausted. You have a payment method on file but auto-reload is disabled, so runs paused once your balance went past the overdraft buffer. Top up balance → · Enable auto-reload →
|

Why
Adding a widget type to the dashboard builder meant ~35 edits across 4 packages. The widget type wasn't a type — it was a string smeared across:
state.visualizationinwidget-builder-utils.ts(864 lines)showXflags in the 650-line pickerRecord<string, Component>renderer registry that fell back to a line chart on a typoVisualizationTypeended in| (string & {}), so TypeScript gave zero exhaustiveness checking anywhere.What
A widget type is now one file plus one registry line, enforced by the compiler.
Layer 1 —
packages/domain/src/http/widget-types.ts. The type-agnostic table: label, persistedvisualization, canonicalchartId, default grid layout, template height, raw-SQL display type, owned display keys, group-by requirement, MCP exposure, Perses kinds. The API, MCP tools, Perses importer, web store and portable-dashboard importer all read it.Layer 2 —
apps/web/src/components/dashboard-builder/widgets/types/.Record<PanelType, WidgetTypeDefinition>, exhaustive on purpose: adding aPanelTypeis a compile error until the type has a renderer, icon, settings panel, preset preview, and the three lowering functions (buildDataSource/buildDisplay/validate).Composition cleanups
isX+ 2showXbooleans become per-typeConfigPanels composed from aWidgetSettings.*compound namespace that reads state from context, not props.chart/pie/histogram/heatmap/funnel) collapse into one factory.WidgetShellalready resolved them fromWidgetActionsContext; only the widget lab passed them, and it now mounts aWidgetActionsScopeinstead.(string & {})dropped fromVisualizationType.widget-builder-utils.tswidget-settings-bar.tsxchart-picker.tsxNet −2,063 lines.
Bugs fixed on the way
These were all drift between the hand-maintained copies:
KNOWN_VISUALIZATIONS(MCP) was missingmarkdown— the comment claimed it was "kept in lockstep" by hand.VIZ_ICONS/VIZ_LABELSknew 4 of 10 kinds.kindliteral knew 6 of 12, so a gauge reported itself as a table.portable-dashboardusedminW: 3where the store used2.thresholdsset on a pie silently did nothing —pie-widget.tsxnever forwarded them. The factory forwards the whole display config.Not in scope
visualizationstaysSchema.Stringon the wire; this is a code-organization change.useImperativeHandleinwidget-query-builder-page.tsxstays. Movingapply/isDirtyinto the builder context needs the raw-SQL draft and initial-mode refs moved into atom scope too — a risky rework of the Run Preview / dirty machinery, orthogonal to widget extensibility.template-live-preview.tsx'sMIN_HEIGHTstays hand-written — those are measured pixel floors for a thumbnail, not grid units.The working tree already contained the
refreshIntervalSecondsfeature when this work started. It touches 9 files, 5 of which this change also edits (types.ts,use-dashboard-store.ts,portable-dashboard.ts, domaindashboards.ts,v2/dashboards.ts), so it could not be split into its own commit without producing a non-compiling intermediate. It ships here as a single commit rather than a fake split.Its two outstanding failures are fixed as part of this PR:
version-list-item.tsxwas missing therefresh_interval_changedhistory label (typecheck error).v2-contract.test.ts's dashboard fixture was missing the now-requiredrefresh_interval_secondskey.Verification
@maple/domain,@maple/api,@maple/web.@maple/domain392 pass ·@maple/api61 pass (dashboard/perses scope) ·@maple/web281 pass (dashboard/query-builder/widget scope).widget-type-cycle.test.tswalks all 144 ordered type-switch pairs asserting no foreignchartIdand no leaked display key, plus histogram endpoint routing (MAP-49), stat sparkline nesting, and the group-by rules. These paths were previously only ever checked by clicking./widget-labrenders all 12 types; cycled a widget through every entry in the Type picker and captured each rail's field set — all match the old boolean-gated behaviour exactly; Line→Pie renders a real pie rather than a leftover line chart; the widget card menu (Edit/Clone/Create alert/Delete) still resolves from context after the prop removal; the Add Widget picker shows all 10 registry-derived tabs and 37 presets.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.