Repository navigation
Implement the multi-dashboard workspace and Dashboard/Library UX end to end #430
Description
Mission
Deliver the complete multi-Dashboard workspace and Dashboard/Library UX through the staged issues #424–#429.
This issue is the authoritative epic for sequencing, integration rules, completion status and final acceptance. Child issues contain the detailed implementation contracts for each phase.
Current status
- Introduce StoredWorkspaceV3 with a dashboard collection and no UI change #424 — StoredWorkspaceV3 Dashboard collection, implemented by feat: StoredWorkspaceV3 — a Dashboard collection, with no UI change #432
- Add dashboard selection state and a full-size dashboard work surface #425 — ID-selected full-size Dashboard surface, implemented by feat(#425): Dashboard as a full-size main work surface, selected by stable id #436
- Add a Databases/Dashboards switcher and dashboard hierarchy tree #426 — Databases/Dashboards switcher and read-only Dashboard tree, implemented by feat(#426): Databases/Dashboards switcher and a read-only Dashboard hierarchy tree #444 (review follow-ups in fix(#426): search-forced tree rows, phantom current member, pure-module placement #445)
- Separate Library queries from dashboard-owned query copies #427 — Dedicated Dashboard-owned query copies and Library projection — in review as feat(#427): pure Dashboard query-ownership model (1/3) #446 (pure ownership model) + the ownership/Library PR
- Add Library drag assignment to Dashboard panels and variables #428 — Library assignment to Dashboards — drag path shipped in PR feat(#428): assign a Library query to a Dashboard by dragging it #484 (merged 2026-07-27); acceptance bullet 9's keyboard
Add to dashboard…chooser shipped in PR feat(#483): add keyboard Library assignment chooser #516 (merged 2026-07-28) - Dashboard tree management: direct Dashboard and Panel actions #429 — all Dashboard-tree management and click operations: edit/delete actions, the revised Dashboard-row gesture split, the one create path, and the repair-planner decision (absorbed Separate Dashboard-tree expansion from opening the Dashboard #472, openSavedQuery switches surfaces before resolving the id, and stays silent when it fails #443, planSavedQueryMutation still has no production caller; deleteSaved rejects instead of offering its repairs #449, A Dashboard the UI cannot reach can make a saved query permanently undeletable #431, A whitespace-only tile title survives into accessible names #476, Reconcile the two Dashboard-creation paths behind one action #481)
File-menu reconciliation (#452 → #463)
- Unify File menu across Query and Dashboard surfaces #452 unified the Query and Dashboard File menus into one control (shipped).
- Refactor File menu for multi-Dashboard creation, import, and export #463 finishes the job for a multi-Dashboard workspace: New dashboard…
exists, Import dashboard… is additive on every surface, and Export
dashboard… resolves an exact Dashboard from the workspace rather than
requiring one on screen — shipped in PR feat(#463): File menu creates, imports and exports Dashboards workspace-wide #482. It removed
planImportDashboard's compatibility-slot / explicit-target / create-first
targeting outright, so no import path can writedashboards[0]any more.
Reconciling the empty-Dashboard placeholder's own Create dashboard button
with that command was triaged out of Reconcile the two Dashboard-creation paths behind one action #481 and absorbed into Dashboard tree management: direct Dashboard and Panel actions #429, which
already owns Dashboard-row/document-level metadata (its rename pencil is the
create action's sibling). Reconcile the two Dashboard-creation paths behind one action #481 is closed. Triage findings: the placeholder
renders only for an empty collection, so the divergence is visible as the
FIRST Dashboard in a fresh workspace being titledDashboardwith no prompt;
andreconcileMainSurface(missing/duplicate id → Query) is what keeps the
placeholder's non-empty-collection refusal unreachable. Dashboard tree management: direct Dashboard and Panel actions #429 narrows its
"creation" non-goal to cover exactly this reconcile — no new entry points.
Related follow-up defects/debt:
- A Dashboard the UI cannot reach can make a saved query permanently undeletable #431 was absorbed into Dashboard tree management: direct Dashboard and Panel actions #429 and closed (2026-07-27). Add a Databases/Dashboards switcher and dashboard hierarchy tree #426 made every Dashboard reachable, which retired its original "UI cannot reach it" framing; what remained — a delete rejected by a reference on another Dashboard, with an unactionable diagnostic — is now a Dashboard tree management: direct Dashboard and Panel actions #429 requirement alongside the planSavedQueryMutation still has no production caller; deleteSaved rejects instead of offering its repairs #449 repair-planner decision.
- Importing a Dashboard bounces the user back to the Query surface #454 is fixed on
mainby Refactor File menu for multi-Dashboard creation, import, and export #463 (PR feat(#463): File menu creates, imports and exports Dashboards workspace-wide #482) and closed:planImportDashboardno longer remints-and-replaces under a new id, it appends and reportsimportedDashboardId, and the File menu reveals it. - Fix Dashboard focus and scrolling for flow-layout KPI tile groups #438 (flow-layout KPI focus/scroll) is a prerequisite for Dashboard tree management: direct Dashboard and Panel actions #429's focus-tile gestures, not part of it. Flow layout's KPI band members have no delete, grip, resize or title chrome #475 (flow KPI band chrome) stays separate too, though Dashboard tree management: direct Dashboard and Panel actions #429's tree trash supplies the delete path Flow layout's KPI band members have no delete, grip, resize or title chrome #475 lists as missing.
- Keyboard-accessible "Add to dashboard…" command for Library queries #483 (keyboard
Add to dashboard…) belongs to Add Library drag assignment to Dashboard panels and variables #428's lineage — a Library-row affordance over Add Library drag assignment to Dashboard panels and variables #428's assignment commands — not to Dashboard tree management: direct Dashboard and Panel actions #429's Dashboard-tree scope; shipped in PR feat(#483): add keyboard Library assignment chooser #516. - Retire the #425 star gate when #427 separates Dashboard-owned query copies #434 must be closed by Separate Library queries from dashboard-owned query copies #427 when favourite/membership coupling and the Add dashboard selection state and a full-size dashboard work surface #425 star gate are removed.
Authoritative implementation order
#424 Storage collection DONE
-> #425 Selected full-size surface DONE
-> #426 Read-only Dashboard tree DONE
-> #427 Ownership migration + Library IN REVIEW
-> #428 Assignment
-> #429 Edit/delete
-> final integration cleanup
Reconciled (#427, 2026-07-25): one scope deviation
#427's "make sourceQueryId required at an explicit persisted-contract boundary"
is NOT implemented, deliberately. A From/To time-range pair and a free-text search
box have no option list and therefore no lossless source query to derive, and
core/time-range.ts only pairs filters that have NO source — so requiring one
would make an authored time range impossible to persist. Eleven such filters ship
across five example bundles (clickhouse-operations, ontime-charts,
shop-charts, iceberg-dba-dashboard, iceberg-catalog-dashboard), which would
all have become un-importable.
The invariant implemented instead is curated => dedicated single-owner source:
a filter that HAS a sourceQueryId must own exactly one filter-role query, while a
plain filter remains valid and owns nothing. Every other #427 acceptance criterion
is unaffected. Whether plain filters should become a distinct persisted control
kind (and curated sources then required) is tracked separately.
This also revises the "Final integration cleanup" item "remove transitional
source-less filter authoring/runtime branches after migration": source-less
filters are a supported final kind, not a transitional state, so only the
FAVOURITE-driven implicit-source branch was removed (#427 replaced it with
name-based matching over Library filter-role queries).
Why #426 precedes #427
Before #427, Dashboard-member queries still appear in the lower Queries list, so they remain reachable even without a tree.
#427 removes Dashboard-owned queries from the lower list and renames that projection to Library. Therefore the Dashboard tree must exist first, otherwise migration would hide queries before another navigation surface could open them.
#426 must support both:
- the transitional pre-Separate Library queries from dashboard-owned query copies #427 model, where queries can be shared and filters can omit
sourceQueryId; - the final post-Separate Library queries from dashboard-owned query copies #427 model, where each member owns one dedicated query and curated filters have sources.
Phase contracts
Phase 1 — #424: Storage collection — complete
Implemented by #432.
Delivered:
- canonical
StoredWorkspaceV3withdashboards[]; - V2 decode migration;
- complete collection validation and persistence;
- full import/export preservation;
- ID-addressed Dashboard replacement helpers;
- temporary compatibility Dashboard selection;
- temporary favourite-driven membership of the first Dashboard.
No further #424 work is required for the epic sequence.
Phase 2 — #425: Selected Dashboard surface — complete
Implemented by #436.
Delivered:
- selected Dashboard session state by stable ID;
- Query/Dashboard main-surface modes;
- full right-hand Dashboard surface while preserving the sidebar;
- preserved Query editor/results state across surface switching;
- View/Edit mode;
- tile/filter focus requests;
- selected-ID import/export;
- fail-closed explicit import targeting;
- selection reset across workspace changes;
- generation-safe surface lifecycle.
#426 may extend the #425 navigation state with retained current-member identity and in-place same-Dashboard focus. That is an integration refinement required by the tree, not a reason to reopen the completed phase.
Phase 3 — #426: Read-only Dashboard tree — next
Implement now.
Scope:
- upper
Databases | Dashboardsswitcher; - persistent role hosts and independent search/scroll/expansion state;
- hierarchy
Dashboard -> Filters -> Panels; - navigation for every stored Dashboard/member;
- transitional support for shared queries and source-less filters;
- exclusive click/double-click/Shift-click arbitration;
- keyboard tree and accessible action alternatives;
- current Dashboard/member styling;
- in-place same-Dashboard member focus without rerunning queries;
- removal of obsolete header SQL Browser/Dashboard buttons;
- no data mutation, ownership migration, drag, pencil or trash.
The lower pane remains Queries | History in this phase.
Phase 4 — #427: Ownership migration and Library
Begins only after #426 is merged.
Scope:
- workspace-wide owner index;
- one dedicated query per panel/filter member;
- migration preserving original queries as Library sources;
- required source-backed curated filters at a versioned contract boundary;
- favourite state fully independent from membership;
- remove the Add dashboard selection state and a full-size dashboard work surface #425 star gate and close Retire the #425 star gate when #427 separates Dashboard-owned query copies #434;
- rename
QueriestoLibraryatomically with the zero-owner projection; - hide owned queries from Library while keeping them openable through Add a Databases/Dashboards switcher and dashboard hierarchy tree #426;
- import normalization and exact-one-owner validation.
Phase 5 — #428: Assignment
Scope:
- drag only standalone Library queries;
- Dashboard/Panels drop creates an independent panel-role copy and tile;
- Filters drop creates an independent filter-role copy and configured filter;
- source remains unchanged in Library;
- atomic validation and commit;
- keyboard-accessible
Add to dashboard…equivalent.
Phase 6 — #429: Edit/delete
Depends on #426 and #427. It may be implemented in parallel with #428 after those dependencies land.
Depends additionally on #447 (phases 1–2, shipped), on #428 (PR #484, merged
2026-07-27) and on #438 for the focus-tile gestures on flow layouts only.
Split into six phases (2026-07-27), listed as a ## Phases checklist in #429:
1 — resolve-before-navigate + blank-title trim; 2 — Dashboard-row gesture split;
3 — create path + rename pencil; 4 — panel-row edit; 5 — panel-row delete;
6 — repair-planner decision. Each phase is a vertical slice (planner + UI + tests
in one PR) rather than planners-first, specifically to avoid repeating the
no-caller-planner problem #449 recorded. Phases 2–5 now build on top of #484's
Library drop-target wiring on the same rows, which they must preserve.
Phase 1 is implemented on fix/resolve-before-navigate-429p1 (9813cf3) and is not
yet pushed or PR'd.
Rescoped in the same pass: #447's hard removal means curated filters no longer
exist, so #429's curated-filter row/pencil/trash sections and its
updateOwnedFilter / removeDashboardFilter commands were deleted rather than
adapted. Panel tiles are now the only members owning a dedicated query; variable
management stays with #447 phase 3 and #465.
Scope — this phase is the single owner of Dashboard-tree management and clicks:
- Dashboard metadata pencil;
- panel metadata editing (curated-filter editing is gone with Replace curated Dashboard filters with inferred Variables and batched option queries #447);
- panel removal plus exactly-owned query deletion;
- layout/fallback/filter-target cleanup;
- exact-owner integrity checks;
- focus/editor/concurrency cleanup;
- the revised Dashboard-row gesture split — chevron expands, name opens View, trailing action edits (absorbs Separate Dashboard-tree expansion from opening the Dashboard #472, revising Add a Databases/Dashboards switcher and dashboard hierarchy tree #426's contract);
- one
createDashboardpath shared by the File menu and the empty-Dashboard placeholder (absorbs Reconcile the two Dashboard-creation paths behind one action #481); openSavedQueryresolving before it navigates (absorbs openSavedQuery switches surfaces before resolving the id, and stays silent when it fails #443);- wire or delete the no-caller repair planner, and close the rejected-delete dead end (absorbs planSavedQueryMutation still has no production caller; deleteSaved rejects instead of offering its repairs #449 and A Dashboard the UI cannot reach can make a saved query permanently undeletable #431);
- trim-before-fallback for titles, so blank-but-truthy never reaches an accessible name (absorbs A whitespace-only tile title survives into accessible names #476).
Final integration cleanup
- remove obsolete compatibility Dashboard assumptions where explicit selection exists;
- remove favourite-membership compatibility code;
- remove transitional source-less filter authoring/runtime branches after migration;
- ensure all user-facing lower-sidebar terminology is Library;
- ensure no owned query appears in Library;
- ensure all Dashboard mutations are ID-addressed and atomic;
- close Retire the #425 star gate when #427 separates Dashboard-owned query copies #434 when its acceptance conditions are satisfied (A Dashboard the UI cannot reach can make a saved query permanently undeletable #431 was absorbed into Dashboard tree management: direct Dashboard and Panel actions #429 and is already closed);
- run schema generation checks, type checks, unit tests, E2E tests, build and bundle checks.
Final product model
Workspace
queries[]
Library query: zero Dashboard owners
Owned query: exactly one panel/filter owner
dashboards[]
Dashboard
filters[]
sourceQueryId -> dedicated filter query
tiles[]
queryId -> dedicated panel query
Final invariants:
- each Dashboard member owns exactly one saved query;
- no saved query is shared among Dashboard members;
- curated filters always have a source query;
- ownership is derived only from Dashboard references;
- favourites never affect Dashboard membership;
- assignment always copies;
- filters render at the top and have no layout position;
- member identity is tile/filter ID, not query ID;
- Dashboard selection is session state, not persisted workspace content.
Final sidebar
Upper
Databases | Dashboards
Lower
Library | History
The header contains non-interactive branding:
Altinity® SQL Browser
Dashboard View/Edit and Back-to-query controls remain in the Dashboard surface toolbar.
Final interaction grammar
Dashboard row
| Gesture | Action |
|---|---|
| Click | Expand/collapse |
| Double-click | Open View |
| Shift-click | Open Edit |
| Pencil | Edit title/description |
Panel row
| Gesture | Action |
|---|---|
| Click | Open owned query |
| Double-click | Open View and focus tile |
| Shift-click | Open Edit and focus tile |
| Pencil | Edit owned query metadata |
| Trash | Remove tile and owned query atomically |
Filter row
| Gesture | Action |
|---|---|
| Click | Open owned source query |
| Double-click | Open View and focus filter |
| Shift-click | Open Edit and focus filter |
| Pencil | Edit filter and source metadata |
| Trash | Remove filter and owned source query atomically |
Mouse actions must have keyboard-accessible equivalents.
Cross-phase integration rules
- Tree/member state is keyed by workspace + Dashboard + tile/filter IDs, never query names or array positions.
- Separate Library queries from dashboard-owned query copies #427 migration preserves Dashboard, tile and filter IDs so Add a Databases/Dashboards switcher and dashboard hierarchy tree #426 state remains stable.
- Add Library drag assignment to Dashboard panels and variables #428/Dashboard tree management: direct Dashboard and Panel actions #429 mutate through pure application planners; tree views do not splice documents directly.
- Every user mutation builds and validates one complete V3 candidate and commits atomically.
- Only the targeted Dashboard revision increments for Dashboard-content mutations.
- Query-metadata-only changes do not increment Dashboard revision unless an existing documented aggregate rule requires it.
- All non-target Dashboards remain canonical-value unchanged.
- Stale dialogs, drags, focus requests and async callbacks cannot act on a replacement workspace/surface.
Final acceptance
The epic is complete when:
- every stored Dashboard is reachable and navigable;
- the lower pane is Library/History and contains only standalone queries;
- each panel/filter has one dedicated query;
- source-less filters have been migrated or rejected precisely at the migration boundary;
- favourites have no membership side effects;
- Library-to-Dashboard assignment always copies;
- Dashboard/member edit and delete operations are complete and atomic;
- migration preserves user data, order, IDs, layout and revisions;
- import/export round trips all Dashboards and owned queries;
- no hidden/unreachable Dashboard can permanently block a repairable operation;
- all child-issue tests and integrated journeys pass;
- database tree, Query editor, results, History and existing Dashboard execution have no unintended regressions.
Immediate next action
Implement #426 against current main after #432 and #436. Do not start #427 until #426 has landed and every Dashboard-owned/shared query is reachable through the tree.
Activity