You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Promote the saved-query visualization config from "a chart, plus special cases" to Panels — a first-class, extensible set of visualization types, designed in the
workbench bottom drawer and rendered identically as dashboard tiles.
v1 union (this issue):
panel.cfg.type ∈ bar | hbar | line | area | pie ← chart family (Chart.js, existing cfg: column indices + key)
| table ← D9 (#164; code pulled from PR #168)
| logs {time, msg, level} ← D9 logs renderer; roles by column name
| text {content} ← markdown, no SQL required
Follow-up arms on this registry (additive — the union is designed to grow): kpi (#154) · filter (#160) · setup (#175). This issue does not ship them, not even
as stubs — a selectable type that renders nothing is a bad product state, and their
acceptance criteria live where their domain logic lives.
(Rewritten 2026-07-11, twice: first from "logs as a chart-cfg type" into the registry —
KPI, filters and logs are the "third opinionated view type" that CLAUDE.md rule 5 and this
issue's own non-goals reserved the registry for; then reconciled against an external
implementation review that caught the migration, precedence, versioning, text-authoring,
registry-API and dispatch-order gaps below.)
Base: D9 lands inside this issue
PR #168 will not be merged. Its code (table/logs renderers, per-tile result caps, slot
lifecycle) is pulled into this issue's implementation. #164 stays open as the acceptance
reference for that slice and is closed by this issue's PR ("Closes #164").
Cfg field policy (pinned)
Roles: the chart family keeps its existing column indices + panel.key
(schemaKey) — zero migration for every existing chart config. logs (and later kpi)
reference columns by name — natural for their name-based conventions, robust to column
reordering — and carry no key (a failed name lookup is the mismatch signal). table/text (and later filter) have no schema-bound fields and no key.
Mismatch policy: a schema change retains the explicit type and re-derives the roles
within it (chart: re-run the axis heuristic for that chart type; logs: convention
defaults), with a small "roles re-detected" hint. autoPanel + a diagnostic only when the
type is impossible for the result shape.
Unknown/extra cfg fields are ignored and preserved (round-trip safe). This is the
additive forward-compatibility mechanism — a newer build's fields survive an older build's
edit — and it makes the view:'table' migration below lossless.
Dirty semantics: a user's panel-cfg edit marks the tab/Library dirty exactly like a SQL
edit; cfg derived purely by autoPanel (untouched by the user) never dirties anything.
Panel type semantics (v1 arms)
chart family — exactly today's chart.cfg, untouched.
table — D9's table tile as an explicit choice (subsumes view:'table' tile behavior);
surface-local sort/width state.
logs — {time, msg, level} by name; renderer is D9's logs module, unchanged.
text — {content}: Markdown lives in panel.cfg.content (Grafana's model — panel
options own the content), edited in the Panel tab's config area (a textarea) with the
rendered preview below; description stays a description. Rules: sql: '' is allowed for this type only (the save guard relaxes per-type); if SQL is present, workbench
Run executes it normally and the dashboard ignores the result; Markdown export emits the
content, script export skips SQL-less entries; drag, share-link, and saved-row click
behave like any other entry. Rendering: an in-house safe Markdown subset — headings, bold/italic, lists, links,
inline code — built as DOM (never innerHTML), raw HTML disabled, pure core module at
100%. No new runtime dependency (hard rule 4); full-Markdown via a bundled library was
considered and rejected for a niche panel.
Explicit panels never vanish: a zero-row explicit table/logs panel renders a visible
empty state ("0 rows"); only unconfigured empty results count toward the "N not shown"
skip tally. (The same principle binds #154: an explicit kpi with 0/N rows renders an honest
warning state, not a silent fallback; reducers are a future extension.)
Workbench: the Panel drawer tab
The results-pane Chart tab becomes "Panel": a Type picker (Bar … Pie · Table · Logs ·
Text, growing as follow-up arms land) plus the per-type config row. view (the remembered
drawer tab) becomes table | json | panel.
Previews never execute SQL — the workbench execution model stays "SQL runs on Run,
nowhere else." Every preview renders from the tab's last explicit Run result (the text
preview renders cfg.content and needs no result at all); switching type or editing cfg
fires nothing; an empty-preview hint shows when no Run has happened. Stale previews resolve
on the next Run, exactly like the chart preview today. (When #160's filter arm lands, its
preview follows the same rule — options come from the last Run's rows, the control is inert,
and its param field is a combobox suggesting parameter names harvested via readStatementParams across the whole Library, since the target param normally does not
appear in the option query's own SQL. Auto-executing option queries exist only on the
dashboard.)
Detached Data Pane: render-only — it can display the source tab's panel, but keeps no
panel editor (v1 scope).
Library format & migration
This is an additive version-1 change.panel: {cfg, key?} is a new optional field; version stays 1 — imports hard-reject version > 1 (saved-io.js), so bumping would
break every older build. The forward-compatibility story is: old builds drop the unknown panel field; this build ignores-and-preserves unknown cfg fields.
One pure upgradeSavedEntry(), applied at every ingress — not just file import:
localStorage startup (asb:saved → state.savedQueries), JSON import, replace/append/
merge, tab restoration, dashboard snapshot creation, and share-link decoding.
view: 'table'with a latent chart cfg → panel: {type:'table', …chart role fields retained} — D9's table-over-chart precedence is preserved (no visual change), the old
chart roles survive as extra fields (Q6), and switching the type back to a chart prefills
them. Lossless.
view: 'chart' → view: 'panel'.
Rollback safety — dual-write for one release: save/export/share write panelplus a
legacy chart mirror for chart-family types, so a user who rolls back to an older build
(or shares to one) keeps their charts; only table/logs/text degrade to heuristics. The
mirror is removed in the next minor.
Share links: encodeShare writes {sql, panel} (+ mirror); decodeShare accepts
legacy {sql, chart} and raw-SQL links via upgradeSavedEntry.
Registry contract
Strong enough for the real renderers (Chart.js must be destroyed; tables hold sort/width
state; workbench and dashboard hold lifecycles differently):
Prep refactor first: extract the chart/grid/footer renderers out of results.js into
their own modules before introducing panels.js — otherwise results → panels → logs →
results is a circular dependency (the D9 logs module already imports its footer from results.js).
Dashboard: partition before execution
Favorites are partitioned by panel type before any SQL runs (the current
classify-after-execute cannot support no-query panels):
Per-file gates hold. panel-cfg: every arm valid/invalid/normalize/clone;
ignore-and-preserve round-trip; mismatch retains type + re-derives roles (indices+key for
chart, names for logs); impossible-shape → autoPanel + diagnostic; autoPanel ladder incl.
never-auto types. saved-io/state/share: upgradeSavedEntry exercised at each
ingress (startup, import, merge, tab restore, dashboard snapshot, share decode); view:'table'+chart migrates losslessly (table on dashboard, chart roles recoverable);
dual-write mirror present for chart family only; version stays 1; share round-trips legacy
and new. markdown-lite: subset rendering; raw HTML/script/event-handler injection inert
(DOM-based, XSS cases). UI: Panel tab per type; no preview path issues a network
request; text panel saves with empty SQL, Run executes attached SQL when present, exports
behave; explicit zero-row table/logs render empty states and skip the tally; cfg edit
dirties, autoPanel cfg doesn't; dashboard partitions before execution (text renders with
zero queries issued); detached pane renders read-only; D9 table/logs behavior matches #164's
acceptance (regression, since PR #168 is absorbed).
Acceptance criteria
The four v1 panel types render in the workbench Panel tab and as dashboard tiles from
the same registry entry; drawer preview ≡ tile; no preview ever executes SQL.
library.json gains the optional panel field with version staying 1; upgradeSavedEntry runs at every ingress (startup, import, merge, tab restore,
dashboard snapshot, share decode) — a user upgrading in place sees no visual change,
including view:'table'-with-latent-chart entries (table preserved, chart roles
recoverable).
Dual-write mirror: a document saved by this build still shows its charts in the
previous release.
Unknown panel types and unknown cfg fields degrade/preserve per the field policy;
nothing is silently stripped.
Schema changes retain the explicit type and re-derive roles; explicit zero-row
table/logs panels stay visible.
A {type:'text'} panel is authorable in the Panel tab (content textarea), saves with
empty SQL, renders the safe Markdown subset with raw HTML inert.
The dashboard partitions favorites before execution; a text favorite issues zero
queries.
Chart/table/logs behavior byte-equivalent for existing saved queries; coverage gates
hold; no new runtime dependency.
Non-goals
The kpi (#154), filter (#160) and setup (#175) arms — each lands with its own issue on
this registry, including its union arm and saved-io validation; KPI reducers; full Markdown
/ raw HTML; a detached-pane panel editor; per-dashboard layout changes (D4/D7); a
plugin/external-panel mechanism. Later Grafana-catalog types: gauge/bar gauge, heatmap,
histogram, state timeline; not planned: geomap, canvas, traces, flame graph, news.
Sequencing
Implementable now — PR #168 is not merged; its code is pulled here and #164 is closed by
this PR. Then #154 (kpi arm) and #160 (filter arm, with #165/#173) build on the registry; #175 (setup) follows; D7's popover reuses the type picker. Tracked in #149 and #68 Phase 6.
Goal
Promote the saved-query visualization config from "a chart, plus special cases" to
Panels — a first-class, extensible set of visualization types, designed in the
workbench bottom drawer and rendered identically as dashboard tiles.
v1 union (this issue):
Follow-up arms on this registry (additive — the union is designed to grow):
kpi(#154) ·filter(#160) ·setup(#175). This issue does not ship them, not evenas stubs — a selectable type that renders nothing is a bad product state, and their
acceptance criteria live where their domain logic lives.
(Rewritten 2026-07-11, twice: first from "logs as a chart-cfg type" into the registry —
KPI, filters and logs are the "third opinionated view type" that CLAUDE.md rule 5 and this
issue's own non-goals reserved the registry for; then reconciled against an external
implementation review that caught the migration, precedence, versioning, text-authoring,
registry-API and dispatch-order gaps below.)
Base: D9 lands inside this issue
PR #168 will not be merged. Its code (table/logs renderers, per-tile result caps, slot
lifecycle) is pulled into this issue's implementation. #164 stays open as the acceptance
reference for that slice and is closed by this issue's PR ("Closes #164").
Cfg field policy (pinned)
panel.key(
schemaKey) — zero migration for every existing chart config.logs(and laterkpi)reference columns by name — natural for their name-based conventions, robust to column
reordering — and carry no
key(a failed name lookup is the mismatch signal).table/text(and laterfilter) have no schema-bound fields and no key.within it (chart: re-run the axis heuristic for that chart type; logs: convention
defaults), with a small "roles re-detected" hint.
autoPanel+ a diagnostic only when thetype is impossible for the result shape.
additive forward-compatibility mechanism — a newer build's fields survive an older build's
edit — and it makes the
view:'table'migration below lossless.edit; cfg derived purely by
autoPanel(untouched by the user) never dirties anything.Panel type semantics (v1 arms)
chart.cfg, untouched.view:'table'tile behavior);surface-local sort/width state.
{time, msg, level}by name; renderer is D9's logs module, unchanged.{content}: Markdown lives inpanel.cfg.content(Grafana's model — paneloptions own the content), edited in the Panel tab's config area (a textarea) with the
rendered preview below;
descriptionstays a description. Rules:sql: ''is allowedfor this type only (the save guard relaxes per-type); if SQL is present, workbench
Run executes it normally and the dashboard ignores the result; Markdown export emits the
content, script export skips SQL-less entries; drag, share-link, and saved-row click
behave like any other entry.
Rendering: an in-house safe Markdown subset — headings, bold/italic, lists, links,
inline code — built as DOM (never innerHTML), raw HTML disabled, pure core module at
100%. No new runtime dependency (hard rule 4); full-Markdown via a bundled library was
considered and rejected for a niche panel.
Explicit panels never vanish: a zero-row explicit table/logs panel renders a visible
empty state ("0 rows"); only unconfigured empty results count toward the "N not shown"
skip tally. (The same principle binds #154: an explicit kpi with 0/N rows renders an honest
warning state, not a silent fallback; reducers are a future extension.)
Workbench: the Panel drawer tab
The results-pane Chart tab becomes "Panel": a Type picker (Bar … Pie · Table · Logs ·
Text, growing as follow-up arms land) plus the per-type config row.
view(the remembereddrawer tab) becomes
table | json | panel.Previews never execute SQL — the workbench execution model stays "SQL runs on Run,
nowhere else." Every preview renders from the tab's last explicit Run result (the text
preview renders
cfg.contentand needs no result at all); switching type or editing cfgfires nothing; an empty-preview hint shows when no Run has happened. Stale previews resolve
on the next Run, exactly like the chart preview today. (When #160's filter arm lands, its
preview follows the same rule — options come from the last Run's rows, the control is inert,
and its
paramfield is a combobox suggesting parameter names harvested viareadStatementParamsacross the whole Library, since the target param normally does notappear in the option query's own SQL. Auto-executing option queries exist only on the
dashboard.)
Detached Data Pane: render-only — it can display the source tab's panel, but keeps no
panel editor (v1 scope).
Library format & migration
This is an additive version-1 change.
panel: {cfg, key?}is a new optional field;versionstays1— imports hard-rejectversion > 1(saved-io.js), so bumping wouldbreak every older build. The forward-compatibility story is: old builds drop the unknown
panelfield; this build ignores-and-preserves unknown cfg fields.One pure
upgradeSavedEntry(), applied at every ingress — not just file import:localStorage startup (
asb:saved→state.savedQueries), JSON import, replace/append/merge, tab restoration, dashboard snapshot creation, and share-link decoding.
Rules:
chart: {cfg, key}→panel: {cfg, key}(chart-family type, indices untouched).view: 'table'with a latent chart cfg →panel: {type:'table', …chart role fields retained}— D9's table-over-chart precedence is preserved (no visual change), the oldchart roles survive as extra fields (Q6), and switching the type back to a chart prefills
them. Lossless.
view: 'chart'→view: 'panel'.panelplus alegacy
chartmirror for chart-family types, so a user who rolls back to an older build(or shares to one) keeps their charts; only table/logs/text degrade to heuristics. The
mirror is removed in the next minor.
encodeSharewrites{sql, panel}(+ mirror);decodeShareacceptslegacy
{sql, chart}and raw-SQL links viaupgradeSavedEntry.Registry contract
Strong enough for the real renderers (Chart.js must be destroyed; tables hold sort/width
state; workbench and dashboard hold lifecycles differently):
Prep refactor first: extract the chart/grid/footer renderers out of
results.jsintotheir own modules before introducing
panels.js— otherwise results → panels → logs →results is a circular dependency (the D9 logs module already imports its footer from
results.js).Dashboard: partition before execution
Favorites are partitioned by panel type before any SQL runs (the current
classify-after-execute cannot support no-query panels):
autoPanelfallback on the result.Heuristic:
autoPanelOne function in core replacing
classifyTile's ladder and D9's interim ranking: savedpanel.cfgwins → log-shaped →{type:'logs'}· chartable →autoChart's pick · else{type:'table'}(single-row →{type:'kpi'}joins when #154 lands).filter/text/setupare never auto-proposed.Files
src/ui/panels.js— the registry;src/ui/chart-render.js/ grid/footerextraction from
results.js(prep refactor).src/core/panel-cfg.js— union validation/normalize/clone (ignore-and-preserve),autoPanel, mismatch/re-derive logic; 100%.src/core/markdown-lite.js— the safe subset renderer (DOM-building, HTMLdisabled); 100%.
src/core/saved-io.js—upgradeSavedEntry, union arms, dual-write mirror.src/state.js— upgrade at localStorage startup; per-type save guard (sql:''fortext); dirty wiring.
src/core/share.js— panel encode/decode + legacy accept.src/ui/app.js/src/ui/results.js/src/ui/dashboard.js— Panel tab, dispatchthrough the registry, partition-before-execution; D9 code pulled from PR feat: dashboard table/logs tiles + per-tile result caps (#149 D9) #168.
Tests
Per-file gates hold.
panel-cfg: every arm valid/invalid/normalize/clone;ignore-and-preserve round-trip; mismatch retains type + re-derives roles (indices+key for
chart, names for logs); impossible-shape → autoPanel + diagnostic; autoPanel ladder incl.
never-auto types.
saved-io/state/share:upgradeSavedEntryexercised at eachingress (startup, import, merge, tab restore, dashboard snapshot, share decode);
view:'table'+chart migrates losslessly (table on dashboard, chart roles recoverable);dual-write mirror present for chart family only; version stays 1; share round-trips legacy
and new.
markdown-lite: subset rendering; raw HTML/script/event-handler injection inert(DOM-based, XSS cases). UI: Panel tab per type; no preview path issues a network
request; text panel saves with empty SQL, Run executes attached SQL when present, exports
behave; explicit zero-row table/logs render empty states and skip the tally; cfg edit
dirties, autoPanel cfg doesn't; dashboard partitions before execution (text renders with
zero queries issued); detached pane renders read-only; D9 table/logs behavior matches #164's
acceptance (regression, since PR #168 is absorbed).
Acceptance criteria
the same registry entry; drawer preview ≡ tile; no preview ever executes SQL.
library.jsongains the optionalpanelfield withversionstaying 1;upgradeSavedEntryruns at every ingress (startup, import, merge, tab restore,dashboard snapshot, share decode) — a user upgrading in place sees no visual change,
including
view:'table'-with-latent-chart entries (table preserved, chart rolesrecoverable).
previous release.
nothing is silently stripped.
table/logs panels stay visible.
{type:'text'}panel is authorable in the Panel tab (content textarea), saves withempty SQL, renders the safe Markdown subset with raw HTML inert.
queries.
autoPanelreplacesclassifyTile+ D9's interim ranking; D9 table/logs behavior ispreserved (**Phase D9 — table tiles + logs view**: saved view:'table' + non-chartable fallback render as table/logs tiles, server row cap for all tiles #164's acceptance passes; PR feat: dashboard table/logs tiles + per-tile result caps (#149 D9) #168 closed unmerged, **Phase D9 — table tiles + logs view**: saved view:'table' + non-chartable fallback render as table/logs tiles, server row cap for all tiles #164 closed by this PR).
hold; no new runtime dependency.
Non-goals
The
kpi(#154),filter(#160) andsetup(#175) arms — each lands with its own issue onthis registry, including its union arm and saved-io validation; KPI reducers; full Markdown
/ raw HTML; a detached-pane panel editor; per-dashboard layout changes (D4/D7); a
plugin/external-panel mechanism. Later Grafana-catalog types: gauge/bar gauge, heatmap,
histogram, state timeline; not planned: geomap, canvas, traces, flame graph, news.
Sequencing
Implementable now — PR #168 is not merged; its code is pulled here and #164 is closed by
this PR. Then #154 (kpi arm) and #160 (filter arm, with #165/#173) build on the registry;
#175 (setup) follows; D7's popover reuses the type picker. Tracked in #149 and #68 Phase 6.