Skip to content

Dashboard tree: replace overflow menus with direct Panel focus, edit and delete actions (#429 phase 4) #494

Description

@BorisTyshkevich

Part of #429revised Phase 4.

This issue supersedes #429's previous split between:

  • Phase 4: Panel metadata pencil;
  • Phase 5: Panel delete;
  • the Dashboard/Panel overflow-menu action chrome.

The row chrome, edit command, delete command and focus navigation must land as one vertical slice so the tree is never left with two competing interaction models.

Dependencies

UX decision

Remove the overflow icon from both Dashboard and Panel rows.

Dashboard and Panel rows should expose their important operations directly, following the Library Query row's pencil/trash vocabulary instead of hiding actions in a menu.

Dashboard row

Target Action
Disclosure chevron Expand/collapse the Dashboard hierarchy only
Dashboard icon/name Open the exact Dashboard in View mode
Shift-click / Shift+Enter on the primary row Open the exact Dashboard in Edit mode; retain the existing shortcut
Pencil icon from Phase 3 Edit Dashboard title and description
Trash icon Remove the Dashboard and all its dedicated owned queries atomically after confirmation

There is no button and no Dashboard row action menu. Removing the menu must not change chevron, primary-row, drag/drop or keyboard-tree behaviour.

Panel row

Target Action
Panel icon/name / primary row Open the panel's dedicated query in the Query surface, unchanged
Open-in-Dashboard icon Open the owning Dashboard in View mode, focus the exact tile and scroll it into view
Pencil icon Edit the dedicated query's name and description
Trash icon Remove the tile and its dedicated owned query atomically

Keep the existing panel-row double-click and Shift-click shortcuts unless removing them is required by shared-code simplification:

  • double-click: open Dashboard View and focus the tile;
  • Shift-click / Shift+Enter: open Dashboard Edit and focus the tile.

The direct Open-in-Dashboard button is the discoverable, keyboard-operable path; the existing gestures may remain as shortcuts but must not be the only path.

There is no button and no Panel row action menu.

Direct action presentation

Use the Library Query row as the visual precedent:

  • Icon.pencil() for metadata edit;
  • Icon.trash() for delete;
  • Icon.expand() for Open in Dashboard/focus panel, unless an already-shipped icon is demonstrably clearer in the rendered sidebar.

Required presentation:

  • real <button type="button"> elements;
  • compact trailing action cluster;
  • order: Open in Dashboard → Edit → Delete, keeping the destructive action rightmost;
  • buttons exist in the DOM whenever the operation is part of that row's vocabulary;
  • reveal on row hover and :focus-within, matching Query-row action discoverability;
  • an open dialog/confirmation keeps its trigger visible;
  • narrow sidebar layout must preserve the label's usable width and ellipsis rather than overlapping or wrapping the controls;
  • each button has an independent focus indicator;
  • delete uses the destructive hover/focus treatment;
  • do not add a fourth overflow control.

Accessible names and tooltips:

  • Open <panel name> in dashboard
  • Edit <panel name>
  • Remove <panel name> from dashboard
  • Dashboard pencil: Edit <dashboard name>

The tooltip may be shorter, but the accessible name must identify the target resource.

Event isolation

Every nested action control must be isolated from the row's own gestures.

For mouse and keyboard activation:

  • activating an icon must not run the row click, double-click or Shift-click action;
  • it must not toggle a Dashboard/group disclosure;
  • it must not disturb another row's pending click arbitration;
  • it must cancel only a pending action for its own row when such an action exists;
  • Enter/Space on a nested button must not bubble into the tree-level Enter handler;
  • native button activation must occur exactly once.

Do not rely only on onclick.stopPropagation(): explicitly cover the keydown path used by the tree's delegated keyboard handler.

Open-in-Dashboard command

The focus button must dispatch by stable IDs:

app.openDashboard({
  dashboardId: row.dashboardId,
  mode: 'view',
  focus: { kind: 'tile', id: row.member.id },
});

Requirements:

  • resolve by Dashboard ID + tile ID, never by title, query name, query ID or row index;
  • preserve Dashboard-tree expansion state;
  • opening the already selected Dashboard focuses in place without pushing a duplicate history entry or rerunning the Dashboard unnecessarily;
  • opening a different Dashboard uses the normal Dashboard navigation path;
  • after render, the exact panel is scrolled into view, programmatically focused and receives the bounded navigation highlight;
  • work in View mode for grafana-grid and flow layouts, including flow KPI tiles after Fix Dashboard focus and scrolling for flow-layout KPI tile groups #438;
  • a missing tile reports the existing non-destructive diagnostic and does not focus a different panel;
  • a broken/missing-query tile may still be opened and focused in the Dashboard so its rendered integrity diagnostic is reachable.

Panel pencil action

Open the same metadata editing experience used for Library queries, adapted to a Dashboard-owned query. Prefill:

Name
Description

Canonical fields:

query.spec.name
query.spec.description

Requirements:

  • identify the target by workspace ID, Dashboard ID, tile ID and owned query ID;
  • edit the dedicated owned query only;
  • query ID and SQL are unchanged;
  • Dashboard ownership remains panel;
  • favourite remains absent/false and is not exposed as membership;
  • panel configuration, variants, size hints, time ranges and unknown extension fields are preserved;
  • do not create or modify tile.title or tile.description for normal authoring;
  • preserve imported tile-local title/description overrides;
  • when an imported override masks query metadata, retain established display precedence and show a compatibility note rather than claiming the rendered title will immediately change;
  • validate the complete workspace candidate;
  • commit through the existing serialized read-latest workspace mutation path;
  • do not increment Dashboard revision for a query-metadata-only change unless the existing aggregate contract requires it;
  • refresh the tree, any linked Query tab and the rendered Dashboard after success;
  • Cancel and Escape commit nothing.

If the owned query cannot be proven, the pencil remains rendered but unavailable with aria-disabled="true" and a concise reason. Do not guess a query from the name or another tile.

Panel trash action

Show the existing destructive confirmation pattern:

Remove panel “<panel name>” from “<dashboard name>”?

This also deletes its dedicated query copy.

On confirmation, execute one atomic workspace mutation. The pure command may retain the parent issue's proposed name:

removeDashboardPanel({
  workspace,
  dashboardId,
  tileId,
})

Required transformation:

  1. resolve the target Dashboard by stable ID;
  2. resolve the tile by stable ID;
  3. resolve its query;
  4. verify through the Separate Library queries from dashboard-owned query copies #427 ownership index that the query is owned by exactly this tile;
  5. remove the tile;
  6. remove its placement from the primary layout;
  7. remove its placement from the flow fallback where applicable;
  8. normalise through the active layout plugin;
  9. regenerate the grafana-grid flow fallback where required;
  10. remove the dedicated query from workspace.queries[];
  11. increment the target Dashboard revision exactly once;
  12. replace only the target Dashboard;
  13. validate the complete workspace candidate;
  14. commit once through app.mutateWorkspace.

Variables are inferred from panel SQL. If this was the last declaration of a variable, retain any variableConfigs entry as an orphan; do not delete it as a side effect.

After success:

  • remove the Panel row;
  • preserve Dashboard and Panels-group expansion state;
  • move tree keyboard focus to the next Panel row, otherwise the previous Panel row, otherwise the Panels group;
  • if the tile was the current Dashboard member, keep the Dashboard open and clear the member focus rather than retargeting another tile;
  • if its query was open in the Query surface, use the existing deleted-query reconciliation behaviour;
  • do not modify a Library source query or favourite state.

Cancel/Escape commit nothing.

Dashboard trash action

like Panel's trash action - confirmation and recursive delete of all nested queries.

Ownership and malformed-data safety

Edit/delete is enabled only when the ownership index proves exactly one expected owner: this Dashboard + this tile.

For a missing query, multiple owners, wrong owner kind or mismatched Dashboard/member identity:

  • Open in Dashboard remains available when the tile itself exists;
  • pencil and trash remain visible but unavailable;
  • expose aria-disabled="true" plus a concise tooltip/reason;
  • activating an unavailable control changes nothing and may surface the existing integrity diagnostic;
  • never cascade-delete based on a guessed owner.

Repairing malformed imported data remains with #429's repair-planner phase.

Concurrency and lifecycle

Follow the Phase 3 dialog/mutation pattern:

  • dialogs capture workspace/generation and stable resource IDs, not mutable object references as truth;
  • save/delete re-resolve the target and ownership inside the queued read-latest transform;
  • workspace switch, route change, sign-out or disposal makes a stale dialog inert;
  • if the target was deleted or ownership changed while a dialog was open, commit nothing, close or reconcile the stale UI and report a targeted diagnostic;
  • a successful mutation refreshes the current surface only when its captured surface generation is still current;
  • no detached row/button/dialog may receive late focus after a repaint;
  • concurrent unrelated workspace changes are preserved;
  • repeated activation cannot open duplicate dialogs or submit the same mutation twice.

Model and cleanup requirements

src/application/dashboard-tree-model.ts remains the source of structural decisions. Represent direct action availability/commands in the model; do not re-derive ownership or row capability from DOM classes in src/ui/dashboard-tree.ts.

After Dashboard and Panel overflow menus are removed:

  • remove their menu command entries;
  • remove DashboardTreeMenuItem / buildMenuButton / menu CSS if no remaining production row uses them;
  • remove dead menu-only tests and comments rather than keeping an unused abstraction;
  • preserve the orphan-variable direct trash control;
  • keep explicit row accessible names so nested button labels are not folded into the treeitem name;
  • update CHANGELOG.md because the deployed interaction model changes.

Likely files include:

  • src/application/dashboard-tree-model.ts
  • src/ui/dashboard-tree.ts
  • src/styles.css
  • a pure Dashboard panel mutation module under src/dashboard/application/
  • the existing saved-query metadata mutation/dialog seams
  • tests/unit/dashboard-tree-model.test.ts
  • tests/unit/dashboard-tree.test.ts
  • mutation/service tests
  • tests/e2e/dashboard-tree.spec.js
  • Dashboard focus e2e coverage associated with Fix Dashboard focus and scrolling for flow-layout KPI tile groups #438

The agent must audit current main before choosing exact module names; do not duplicate an existing mutation or dialog primitive.

Required tests

Model

  • Dashboard rows expose no overflow-menu command.
  • Panel rows expose direct focus/edit/delete actions with stable Dashboard/tile/query IDs.
  • malformed ownership keeps focus available and disables edit/delete.
  • no action is derived by title or array position.

Rendering and interaction

  • no .dash-tree-menu-btn is rendered for Dashboard or Panel rows;
  • Dashboard row shows its direct pencil from Phase 3 and no overflow icon;
  • Panel action order is focus, pencil, trash;
  • hover and focus-within reveal the direct controls;
  • each button has exact accessible name, tooltip, type="button" and independent focus treatment;
  • mouse click, Enter and Space on each icon run exactly one intended action and never the row action;
  • action buttons do not expand/collapse the hierarchy;
  • a control on one row does not cancel another Panel row's pending click;
  • primary panel click/double-click/Shift-click behaviour remains correct if retained;
  • row accessible names exclude nested control labels.

Focus navigation

  • exact openDashboard({ dashboardId, mode: 'view', focus: { kind: 'tile', id: tileId } }) dispatch;
  • same-Dashboard focus performs no duplicate route/history write and no unnecessary rerun;
  • different-Dashboard focus navigates then scrolls/focuses the exact tile;
  • missing tile is non-destructive;
  • real-browser coverage proves scrolling/focus for grafana-grid and flow KPI after Fix Dashboard focus and scrolling for flow-layout KPI tile groups #438.

Edit

  • prefill, Save, Cancel and Escape;
  • only name/description change;
  • unknown query fields and panel configuration survive;
  • tile-local overrides survive;
  • linked clean tabs adopt the commit; dirty linked tabs use existing conflict handling;
  • stale/deleted/ownership-changed target commits nothing.

Delete

  • atomic tile + layout + owned-query deletion;
  • Dashboard revision increments exactly once;
  • unrelated Dashboards, queries and variable configs are preserved;
  • malformed ownership refuses deletion;
  • confirmation Cancel/Escape changes nothing;
  • tree focus fallback order is next → previous → Panels group;
  • open Query/Dashboard surfaces reconcile correctly.

Browser accessibility

Use real keyboard events, not .click() substitutes:

  • Tab reaches row → focus icon → pencil → trash and then leaves the row action cluster;
  • Enter/Space activates each nested button without reaching the tree-level Enter handler;
  • screen-reader names keep row and button announcements separate;
  • focus remains visible after any tree repaint caused by an action.

Non-goals

  • redesigning Dashboard View/Edit rendering;
  • persisting tree expansion in workspace data;
  • changing Library Query row behaviour;
  • adding panel duplicate/reorder controls;
  • deleting orphaned variable configuration as a panel-delete side effect;
  • implementing repair choices for malformed/shared ownership;
  • identifying resources by title or collection position.

Acceptance criteria

  • Dashboard rows have no action icon or overflow menu.
  • Panel rows have no action icon or overflow menu.
  • Dashboard metadata edit remains a direct pencil action from Phase 3.
  • [~] Each healthy Panel row exposes direct pencil and trash buttons in that order. The Open-in-Dashboard button is deferred with Fix Dashboard focus and scrolling for flow-layout KPI tile groups #438 (below).
  • Open-in-Dashboard opens View mode and scrolls/focuses the exact tile by stable ID on grafana-grid and flow layouts.
  • Pencil edits only the owned query's name/description and preserves SQL, ownership, presentation and extension data.
  • Trash atomically removes the tile, all layout placements and exactly its dedicated owned query.
  • Dashboard Trash atomically removes all tiles and the correct dashboard
  • Malformed ownership never causes guessed or cascading edits/deletes.
  • Direct buttons are independently named, keyboard-operable and isolated from row/disclosure gestures.
  • Tree expansion, focus ownership and Query/Dashboard surfaces reconcile correctly after every action.
  • Unit and Playwright tests cover mouse, keyboard, accessibility and stale state. Layout-engine coverage is the pure delete transform's (grafana-grid placements + the flow fallback); the browser scroll/focus half rides with the deferred focus button.
  • CHANGELOG.md documents the new direct-action UX.

Verification

The implementation PR must report:

npm test
npx tsc --noEmit
npm run build
npm run test:e2e

Also report the focused #438/Panel-navigation browser specs separately so the scroll/focus contract is visible rather than hidden inside the full matrix.


Delivery status (2026-07-27)

Everything above except the Open-in-Dashboard focus button is implemented; see
the ship log on #429 for the handoff.

The focus button is held back deliberately, per the owner decision taken when this
phase started: it dispatches
openDashboard({ dashboardId, mode: 'view', focus: { kind: 'tile', id } }), which
#438 proves is a no-op on flow-layout KPI tiles (the focus target is a
display: contents host with no box). This issue's own dependency section says not
to ship a known no-op focus action, so the button — and the two acceptance bullets
that describe it — wait for #438.

Note that the capability itself is not a regression: it shipped inside the
menu's Open Dashboard in View/Edit and focus panel items, and those gestures
remain available on a Panel row as double-click and Shift-click. What is deferred
is promoting them to a discoverable button while they are still broken on one
layout engine.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions