Skip to content

Panels: visualization registry + Panel drawer tab + Library panel field #166

Description

@BorisTyshkevich

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):

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:savedstate.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 cfgpanel: {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 panel plus 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):

PANEL_TYPES[type] = {
  controls({ app, result, cfg, onChange }),          // → node; needs result columns for pickers
  renderPanel({ app, entry, result, cfg, surface,    // 'workbench' | 'dashboard'
                state, rerender, readonly, cap }),   // → { node, destroy? }
}

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):

  1. text → rendered immediately, no query;
  2. filter (when Dashboard: query-backed filter options — single-select MVP #160 lands) → the option-query scheduler, never a grid slot;
  3. setup (when Dashboard setup sources: run hidden setup queries before filters and panels #175 lands) → the session wave, hidden;
  4. chart / table / logs (and later kpi) → query-backed grid slots;
  5. invalid/unknown cfg → diagnostic + autoPanel fallback on the result.

Heuristic: autoPanel

One function in core replacing classifyTile's ladder and D9's interim ranking: saved
panel.cfg wins → log-shaped → {type:'logs'} · chartable → autoChart's pick · else
{type:'table'} (single-row → {type:'kpi'} joins when #154 lands). filter/text/
setup are never auto-proposed.

Files

  • new src/ui/panels.js — the registry; src/ui/chart-render.js / grid/footer
    extraction from results.js
    (prep refactor).
  • new src/core/panel-cfg.js — union validation/normalize/clone (ignore-and-preserve),
    autoPanel, mismatch/re-derive logic; 100%.
  • new src/core/markdown-lite.js — the safe subset renderer (DOM-building, HTML
    disabled); 100%.
  • src/core/saved-io.jsupgradeSavedEntry, union arms, dual-write mirror.
  • src/state.js — upgrade at localStorage startup; per-type save guard (sql:'' for
    text); 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, dispatch
    through 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: 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

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions