Skip to content

Add a Databases/Dashboards switcher and dashboard hierarchy tree #426

Description

@BorisTyshkevich

Depends on: #424, #425

Implementation status of dependencies:

Goal

Add a Databases / Dashboards role switcher to the upper-left sidebar and implement a read-only Dashboard hierarchy tree that navigates the full-size Dashboard surface introduced by #425.

This issue establishes reachability and navigation for every stored Dashboard before #427 hides Dashboard-owned queries from the lower list. It must work against the current transitional data model and continue to work unchanged after #427 tightens ownership.

This issue owns:

  • the upper role switcher;
  • Dashboard tree derivation and rendering;
  • Dashboard search;
  • expansion, scroll and keyboard state;
  • Dashboard/member navigation;
  • current-resource styling;
  • removal of the obsolete header-level SQL Browser/Dashboard switch.

It does not mutate Dashboard membership or query ownership.

Delivery-order contract

The authoritative epic sequence is:

#424 storage collection
  -> #425 selected Dashboard surface
  -> #426 read-only Dashboard tree       <- this issue
  -> #427 ownership migration + Library
  -> #428 assignment
  -> #429 edit/delete

#426 intentionally precedes #427. Otherwise #427 would hide Dashboard-owned queries from the lower list before any UI existed to open them.

Therefore this issue must not assume that #427 has already run.

Transitional data rules

Until #427 lands, valid workspaces may contain:

  • one saved query referenced by several Dashboard members;
  • a panel query also visible in the lower Queries list;
  • DashboardFilterDefinitionV1.sourceQueryId absent;
  • favourite-driven membership of the compatibility Dashboard.

The tree is a read-only projection of those references. It must not clone, repair, reject or rewrite them.

After #427:

  • panel/filter references will resolve to dedicated exactly-one-owner query copies;
  • source-backed curated filters will have required source queries;
  • the lower list will become the zero-owner Library projection;
  • favourite state will no longer control membership.

The same tree implementation should naturally consume the post-#427 workspace without a second redesign.

Lower sidebar scope

The lower sidebar remains unchanged in this issue:

Queries | History

#427 owns the atomic semantic change:

Queries -> Library
all saved queries -> zero-owner queries only

Do not rename the lower tab to Library before its contents have Library semantics.

The final visual grammar after #427 will be:

Databases | Dashboards
Library   | History

Upper sidebar architecture

Add an equal-role tab row at the top of the existing upper pane:

Databases · <database-count> | Dashboards · <dashboard-count>

Use the lower switcher's visual language, density and count treatment.

Keep two persistent role hosts inside the upper pane:

upper pane
  role tabs
  databases host
    database search
    schema tree
  dashboards host
    Dashboard search
    Dashboard tree

Switch roles by exposing one host and hiding the other. Do not destroy and reconstruct the schema host merely because the user viewed Dashboards.

This preserves:

  • schema search text and input focus state;
  • schema expansion and lazy-loaded columns;
  • schema scroll position;
  • Dashboard search, expansion and scroll position;
  • the existing upper-pane height, splitter and sidebar-width behaviour.

Requirements:

  • default to Databases for a fresh session;
  • selected upper role is session/UI state, never workspace JSON;
  • keep separate role state per workspace where state is resource-specific;
  • Databases behaviour remains byte-for-byte/behaviourally unchanged where practical;
  • no SQL is executed by rendering or searching the Dashboard tree;
  • no hidden Dashboard viewer/session is created by rendering the tree.

Authoritative data source

Derive the Dashboard tree from the complete committed aggregate:

app.currentWorkspace.dashboards
app.currentWorkspace.queries

Do not derive it from:

app.state.dashboard

state.dashboard is only the selected/compatibility Dashboard projection introduced for the current surface. It does not contain the collection.

The tree must repaint after:

Use an explicit render invalidation signal/seam if needed; do not depend on incidental unrelated signal changes.

Tree hierarchy

Render Dashboards in workspace.dashboards[] order.

Within an expanded Dashboard, render exactly two groups in this order:

Dashboard
  Filters
  Panels

Filters is always before Panels.

Keep both group rows visible while the Dashboard is expanded, including when empty. #428 will use them as stable drop targets.

Dashboard row

Display:

  • disclosure chevron;
  • Dashboard icon;
  • Dashboard title;
  • panel count at the right.

Count:

dashboard.tiles.length

Filters are not included in the Dashboard-row count.

Display-label precedence:

trimmed dashboard.title
otherwise "Untitled dashboard"

Do not normally display the Dashboard ID as its title.

Filters group

Display:

Filters · <dashboard.filters.length>

Each row represents one DashboardFilterDefinitionV1, keyed by:

<workspaceId>:<dashboardId>:filter:<filterId>

Filter display-label precedence:

trimmed filter.label
otherwise resolved source-query spec.name
otherwise filter.parameter
otherwise "Untitled filter"

Current transitional handling:

  • when sourceQueryId is present and resolves, the row can open that query;
  • when sourceQueryId is absent, retain the row as a source-less transitional filter; query-open is unavailable, but Dashboard View/Edit focus navigation remains available;
  • when sourceQueryId is present but unresolved, render an invalid-reference diagnostic row and disable query-open;
  • never crash or hide the Dashboard because one filter reference is bad.

After #427, source-backed curated filters are expected to resolve to dedicated filter-role queries. No #426 tree redesign should be required.

Filters are top Dashboard controls and have no tile/layout position.

Panels group

Display:

Panels · <dashboard.tiles.length>

Each row represents one Dashboard tile, keyed by:

<workspaceId>:<dashboardId>:tile:<tileId>

Resolve the query through tile.queryId.

Display-label precedence:

trimmed tile.title when an imported compatibility override exists
otherwise resolved query.spec.name
otherwise "Untitled panel"

Preserve existing tile-title display semantics; do not create or erase overrides.

Before #427 the same query may appear under several members or Dashboards. Render one row per member and never merge rows by query ID or label.

When tile.queryId is unresolved:

  • retain the tile row;
  • show an invalid-reference icon/label;
  • disable query-open;
  • keep Dashboard View/Edit navigation available so Dashboard diagnostics remain reachable.

Counts and empty states

Upper tab counts:

Databases · schema database count
Dashboards · workspace.dashboards.length

Empty collection:

No dashboards in this workspace.

Empty groups remain visible:

Filters · 0
Panels · 0

Do not create a Dashboard from this tree. Dashboard creation remains outside this issue.

Dashboard search

Dashboard search placeholder:

Search dashboards, filters, panels…

Search case-insensitively across:

  • Dashboard title and description;
  • filter label and parameter;
  • resolved filter source-query name and description;
  • panel query name and description;
  • imported tile-local title and description.

Search behaviour:

  • a child match keeps its Dashboard and group ancestors visible;
  • show matching descendants plus required ancestors;
  • temporarily expose matching paths without mutating saved expansion state;
  • a direct Dashboard match may show its complete hierarchy for context;
  • preserve pre-search expansion and scroll state;
  • clearing search restores that state;
  • search never mutates order or workspace content;
  • search never loads or executes query data.

Prefer presentation-time forced expansion, as the schema tree does, instead of writing temporary matches into the user's expansion sets.

Interaction arbitration

Do not directly copy the schema tree's immediate-single-click detector for member rows. In the schema tree the first click's action is harmless expansion; in the Dashboard tree it would open a query before the second click opens a Dashboard.

Use one deterministic click arbiter per app/tree instance:

  • first primary click schedules the row's single-click action for the double-click window;
  • a second primary click on the same row cancels the scheduled single action and runs exactly one double-click action;
  • Shift-click cancels a pending single action and runs Edit navigation immediately;
  • clicking a disclosure chevron stops propagation and expands/collapses immediately;
  • changing workspace/role or disposing the tree cancels pending click timers;
  • action-menu/button clicks stop propagation and cancel no unrelated row operation.

This avoids Query -> Dashboard flashes and duplicate browser-history entries.

Mouse gesture contract

Dashboard row

Gesture Action
Click Expand/collapse the Dashboard hierarchy
Double-click openDashboard({ dashboardId, mode: 'view' })
Shift-click openDashboard({ dashboardId, mode: 'edit' })

A double-click performs only View navigation and must leave expansion exactly as it was before the gesture.

Filters and Panels group rows

Gesture Action
Click Expand/collapse the group

Double-click and Shift-click have no separate group action.

Panel row

Gesture Action
Click Open the resolved query in the Query surface
Double-click Open Dashboard View and focus tile.id
Shift-click Open Dashboard Edit and focus tile.id

Calls:

openSavedQuery(queryId);

openDashboard({
  dashboardId,
  mode: 'view',
  focus: { kind: 'tile', id: tileId },
});

openDashboard({
  dashboardId,
  mode: 'edit',
  focus: { kind: 'tile', id: tileId },
});

Never focus a panel by query ID.

Filter row

Gesture Action
Click Open the resolved source query when one exists
Double-click Open Dashboard View and focus filter.id
Shift-click Open Dashboard Edit and focus filter.id

Calls for a source-backed filter:

openSavedQuery(sourceQueryId);

openDashboard({
  dashboardId,
  mode: 'view',
  focus: { kind: 'filter', id: filterId },
});

openDashboard({
  dashboardId,
  mode: 'edit',
  focus: { kind: 'filter', id: filterId },
});

For a source-less or unresolved filter, primary query-open is disabled; double-click and Shift-click continue to work.

Extend #425 navigation for tree use

#425 currently treats focus as a one-shot render request and rebuilding the already-open Dashboard is acceptable for direct API use. The tree makes repeated same-Dashboard focusing a normal operation, so this issue must add the following two refinements.

In-place same-Dashboard focus

When Dashboard ID and mode are unchanged and a focus target is supplied:

  • keep the existing Dashboard viewer/editor session;
  • do not rebuild or rerun the Dashboard;
  • do not write another browser-history entry;
  • scroll/focus/highlight the existing tile/filter node through a surface command port;
  • return a non-destructive diagnostic if the member no longer exists;
  • generation-check the command so stale surfaces cannot receive focus.

Suggested seam:

focusDashboardMember(target: DashboardFocusTarget): boolean;

openDashboard() may use this as its same-ID/same-mode fast path. Different Dashboard IDs or mode changes may continue through the normal render transition.

Retained current-member state

The tree must be able to distinguish:

  • the Dashboard currently open;
  • the panel/filter most recently navigated to within it;
  • a pending one-shot DOM focus delivery.

Refactor the Dashboard branch of MainSurfaceState to retain current member identity separately from pending delivery, for example:

{
  kind: 'dashboard';
  dashboardId: string;
  mode: 'view' | 'edit';
  currentMember: DashboardFocusTarget | null;
  pendingFocus: DashboardFocusTarget | null;
}

Equivalent naming is acceptable, but the semantics are required:

  • tree member navigation sets currentMember and requests pending focus;
  • consuming DOM focus clears only pendingFocus;
  • current-member styling remains until another member/Dashboard/query is opened;
  • opening a Dashboard row without a member clears currentMember;
  • switching View/Edit through Dashboard chrome preserves the current member where possible;
  • returning to Query mode clears it;
  • workspace switch, Dashboard removal or missing member clears it;
  • the tree derives current-child styling from application navigation state, not a separate persisted tree selection.

Selection and styling

  • mark the currently open Dashboard from mainSurface.dashboardId;
  • mark the retained current member by Dashboard-local tile/filter ID;
  • keyboard focus styling and current-resource styling must be visually distinct;
  • do not infer current member from query ID;
  • a query opened from a member may optionally retain a subtle non-current origin marker only if an existing convention already supports it;
  • stale current state clears on workspace/resource removal.

Keyboard and accessibility model

Implement an actual keyboard tree rather than referring to a schema-tree convention that does not yet exist.

Container:

role="tree"

Rows:

role="treeitem"
aria-level
aria-expanded for expandable rows
roving tabindex: one visible row at 0, all others -1

Keys:

Key Action
Up / Down Previous / next visible row
Right Expand, or move to first child
Left Collapse, or move to parent
Home / End First / last visible row
Enter Primary single-click action
Shift+Enter Dashboard/member Edit action

Provide a keyboard-reachable trailing action menu for operations otherwise available only through double-click:

Dashboard row:

  • Open in View;
  • Open in Edit.

Panel row:

  • Open query;
  • Open Dashboard in View and focus panel;
  • Open Dashboard in Edit and focus panel.

Filter row:

  • Open source query, when available;
  • Open Dashboard in View and focus filter;
  • Open Dashboard in Edit and focus filter.

The menu button must stop propagation, have an accessible label, and not become confused with the row's disclosure chevron.

Touch-specific behaviour remains out of scope.

Tree UI state

Keep state outside workspace JSON and scope it by workspace identity:

interface DashboardTreeUiState {
  expandedDashboardIds: Set<string>;
  expandedGroups: Set<string>;
  searchText: string;
  scrollTop: number;
  keyboardRowKey: string | null;
}

Map<workspaceId, DashboardTreeUiState>

Requirements:

  • key Dashboards by stable Dashboard ID;
  • key groups by Dashboard ID plus filters/panels;
  • key members by Dashboard ID plus member kind and member ID;
  • never key by array index, title, query ID or displayed label;
  • prune removed resource IDs;
  • ordinary mutations preserve state for surviving IDs;
  • switching back to a workspace may restore its session state;
  • state is never exported or persisted in StoredWorkspaceV3.

Header cleanup

The current header contains an obsolete two-button application-surface switch:

Altinity® | SQL Browser | Dashboard

After this issue, Dashboard selection lives in the upper-left tree. Replace the switch with non-interactive branding:

Altinity® SQL Browser

Requirements:

  • remove the surfaceSwitch buttons from the header;
  • remove the obsolete hidden dashboardNav construction/repaint seam if it has no remaining caller;
  • retain the Dashboard surface toolbar's Back to query and View/Edit controls;
  • retain existing keyboard shortcuts that intentionally navigate surfaces;
  • retain File/workspace controls and connection/user utilities;
  • update tests, ADR/CHANGELOG and accessible names.

Defensive rendering

The persisted workspace validator should normally reject broken references, but the tree must remain robust while data is stale, imported or concurrently changed.

For invalid rows:

  • render the member and its ancestors;
  • show a warning/error icon and safe fallback label;
  • disable only the operation requiring the missing query;
  • keep Dashboard View/Edit/focus navigation available when possible;
  • provide a diagnostic tooltip or accessible description;
  • never throw while constructing the tree.

Suggested code boundaries

Upper sidebar role host

Owns:

  • Databases/Dashboards role tabs;
  • persistent role hosts;
  • role-specific search and scroll restoration;
  • upper-pane repaint invalidation.

Dashboard tree model

Pure derivation of:

  • Dashboard/group/member row identities;
  • resolved query references;
  • transitional source-less filters;
  • labels and counts;
  • search inclusion/forced expansion;
  • invalid annotations;
  • current-resource state.

Dashboard tree view/controller

Owns:

  • DOM and ARIA rendering;
  • click arbitration;
  • expansion;
  • keyboard traversal and action menus;
  • scroll/focus restoration.

Application navigation

Owns:

  • retained current member;
  • pending focus delivery;
  • same-Dashboard in-place focus.

The tree must use public application seams and must not directly manipulate the Dashboard renderer DOM.

Tests

Role switcher

  • Databases and Dashboards tabs render with counts;
  • Databases behaviour remains unchanged;
  • each role preserves independent search, scroll and expansion state;
  • upper-pane resize/splitter behaviour remains unchanged;
  • no workspace JSON change occurs;
  • switching roles does not recreate the schema host or reload columns.

Hierarchy

  • Dashboards render in array order;
  • Filters always precedes Panels;
  • counts are correct;
  • empty groups remain visible;
  • panel rows resolve tile.queryId;
  • source-backed filter rows resolve sourceQueryId;
  • source-less transitional filters remain visible without a query-open action;
  • shared query references produce separate member rows;
  • member state uses Dashboard/member IDs, not query IDs or labels.

Search

  • Dashboard, filter, parameter, source-query, panel-query and tile-override matches work;
  • matching child keeps ancestors visible;
  • paths are temporarily exposed;
  • clearing search restores expansion and scroll;
  • search does not mutate or execute.

Mouse gestures

  • Dashboard click toggles expansion once;
  • Dashboard double-click opens View once without changing expansion;
  • Dashboard Shift-click opens Edit once;
  • panel click opens query only after the single-click window;
  • panel double-click cancels query-open and focuses tile in View;
  • panel Shift-click cancels pending query-open and focuses tile in Edit;
  • filter gestures follow the same exclusivity;
  • source-less filter query-open is disabled;
  • chevron clicks only expand/collapse;
  • no surface flash or duplicate history entry occurs.

#425 integration

  • same-Dashboard same-mode focus does not rebuild the viewer or rerun queries;
  • different Dashboard/mode still navigates normally;
  • pending focus is consumed while current member remains;
  • mode switch preserves current member;
  • Query/workspace/resource changes clear stale member state;
  • tree styling follows navigation from non-tree entry points.

Keyboard/accessibility

  • roving tree focus traverses visible rows;
  • expansion keys and parent/child movement work;
  • Home/End work;
  • Enter performs primary action;
  • Shift+Enter performs Edit navigation;
  • action menus expose View/Edit/focus alternatives;
  • counts/icons are not unlabeled focus targets.

Invalid data

  • unresolved panel query renders a diagnostic row;
  • unresolved filter source renders a diagnostic row;
  • absent transitional source is not treated as an unresolved ID;
  • disabled query-open does not fire;
  • Dashboard navigation remains available where possible.

Header and regressions

  • header renders Altinity® SQL Browser without surface buttons;
  • obsolete Dashboard nav seam is removed;
  • Dashboard toolbar Back/View/Edit remains;
  • schema-tree click/double-click/Shift-click/search/expansion remains unchanged;
  • lower Queries/History pane remains unchanged;
  • opening a query returns to Query mode;
  • rendering the tree never executes hidden Dashboards.

Acceptance criteria

  • The upper sidebar has a Databases/Dashboards role switcher with independent persistent-in-session role state.
  • Every stored Dashboard is reachable in a read-only hierarchy: Dashboard -> Filters -> Panels.
  • The tree correctly handles the current pre-Separate Library queries from dashboard-owned query copies #427 shared/source-optional model and the future post-Separate Library queries from dashboard-owned query copies #427 dedicated-owner model.
  • Search, counts, expansion, scroll, keyboard navigation and invalid-reference rendering are complete.
  • Click/double-click/Shift-click operations are mutually exclusive and follow the specified contract.
  • Same-Dashboard member focus is in-place and does not rerun the Dashboard.
  • Current Dashboard/member styling derives from application navigation state.
  • Databases/schema behaviour is unchanged.
  • The obsolete header surface switch is removed.
  • No drag, pencil, trash, create, delete, reorder or ownership mutation is introduced.

Non-goals

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions