Skip to content

feat(dashboards): make the widget builder a widget-type framework - #282

Merged
Makisuo merged 2 commits into
mainfrom
feat/widget-type-framework
Jul 30, 2026
Merged

feat(dashboards): make the widget builder a widget-type framework#282
Makisuo merged 2 commits into
mainfrom
feat/widget-type-framework

Conversation

@Makisuo

@Makisuo Makisuo commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

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:

  • four if-chains over state.visualization in widget-builder-utils.ts (864 lines)
  • nine boolean flags gating one 592-line settings form
  • nine more parallel showX flags in the 650-line picker
  • four forked copies of the default grid size (web store, portable-dashboard importer, MCP tools, Perses importer)
  • a Record<string, Component> renderer registry that fell back to a line chart on a typo

VisualizationType ended 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, persisted visualization, canonical chartId, 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 a PanelType is a compile error until the type has a renderer, icon, settings panel, preset preview, and the three lowering functions (buildDataSource / buildDisplay / validate).

Composition cleanups

  • The settings rail's 7 isX + 2 showX booleans become per-type ConfigPanels composed from a WidgetSettings.* compound namespace that reads state from context, not props.
  • The five near-identical chart renderers (chart/pie/histogram/heatmap/funnel) collapse into one factory.
  • The five dead action props come off all ten renderers — WidgetShell already resolved them from WidgetActionsContext; only the widget lab passed them, and it now mounts a WidgetActionsScope instead.
  • (string & {}) dropped from VisualizationType.
before after
widget-builder-utils.ts 864 288
widget-settings-bar.tsx 592 35
chart-picker.tsx 650 222
chart-family renderers 5 files, ~280 one factory, ~120
edit sites for a new type ~35 across 4 packages 1 file + 1 registry line + 1 domain entry

Net −2,063 lines.

Bugs fixed on the way

These were all drift between the hand-maintained copies:

  • KNOWN_VISUALIZATIONS (MCP) was missing markdown — the comment claimed it was "kept in lockstep" by hand.
  • The chat approval renderer's VIZ_ICONS/VIZ_LABELS 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.
  • thresholds set on a pie silently did nothingpie-widget.tsx never forwarded them. The factory forwards the whole display config.

Not in scope

  • No persisted shape change and no migration. visualization stays Schema.String on the wire; this is a code-organization change.
  • The useImperativeHandle in widget-query-builder-page.tsx stays. Moving apply/isDirty into 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's MIN_HEIGHT stays hand-written — those are measured pixel floors for a thumbnail, not grid units.

⚠️ For the reviewer: this bundles in-flight work

The working tree already contained the refreshIntervalSeconds feature when this work started. It touches 9 files, 5 of which this change also edits (types.ts, use-dashboard-store.ts, portable-dashboard.ts, domain dashboards.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.tsx was missing the refresh_interval_changed history label (typecheck error).
  • v2-contract.test.ts's dashboard fixture was missing the now-required refresh_interval_seconds key.

Verification

  • Typecheck clean on @maple/domain, @maple/api, @maple/web.
  • @maple/domain 392 pass · @maple/api 61 pass (dashboard/perses scope) · @maple/web 281 pass (dashboard/query-builder/widget scope).
  • New widget-type-cycle.test.ts walks all 144 ordered type-switch pairs asserting no foreign chartId and 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.
  • Browser pass against a live dashboard: /widget-lab renders 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.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

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.
@pullfrog

pullfrog Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

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 →

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

🍁 Maple PR preview

Note

Preview resources were removed when this pull request closed.

Final commit 937f8d9 · View workflow run

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.
@pullfrog

pullfrog Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

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 →

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

@Makisuo
Makisuo merged commit 16386cd into main Jul 30, 2026
13 checks passed
@Makisuo
Makisuo deleted the feat/widget-type-framework branch July 30, 2026 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant