Skip to content

feat(#544): collapse the tile head into a ⋯ menu, atomic delete in every style (closes #537) - #545

Merged
BorisTyshkevich merged 4 commits into
mainfrom
feat/tile-head-overflow-menu-537
Jul 29, 2026
Merged

feat(#544): collapse the tile head into a ⋯ menu, atomic delete in every style (closes #537)#545
BorisTyshkevich merged 4 commits into
mainfrom
feat/tile-head-overflow-menu-537

Conversation

@BorisTyshkevich

Copy link
Copy Markdown
Collaborator

Closes #537. Implements #544.

What changed

PR #538 (#535) left a Dashboard panel tile head carrying five controls — grip,
duplicate, widen, expand, delete — sharing one flex row with the tile title. Every
action is flex-shrink: 0, so the title ellipsizes to nothing first, and a tile a
single grid column wide (~100px at twelve columns) has no row left to compete for.

Mode Head
Edit grip · title · widen · ⋯
View unchanged — the single direct expand icon, no menu

The always lists all four actions, in the design's order, with remove last
behind a separator: Duplicate panel, Widen (carrying widenLabel's dynamic
destination text), Open in Workbench and run, Remove tile. A row that cannot
run is listed and disabled with a sentence saying why — inside a menu the row is
the explanation (#452's argument), so nothing is merely absent and the vocabulary
never changes with the layout style.

The inline widen is a shortcut on top of its row: a CSS container query on the tile
head withdraws it below ~236px of head content box (≈260px of tile). Verified live —
a span-1 grid tile measures 101px, where the inline button is display: none and the
menu still offers "Widen to 2 columns".

The consistency fix

Delete was gated twice to the grafana-grid engine — a .dash-gg-grid CSS
ancestor scope and an activeEngine === 'grafana-grid' check inside its own click
handler — so Report, 2 columns and 3 columns had no delete at all, and a flow KPI
band member had none under any style. An always-present menu row removes both gates;
a band member gains delete for the first time.

#537, absorbed rather than deferred

The tile-header trash dispatched the document-only remove-tile command, which
leaves workspace.queries untouched. Since #427 makes every panel tile the sole
owner
of a saved-query copy, that left the copy with zero owners — which is exactly
what makes a query a Library query — so a deleted panel reappeared as an apparently
standalone Library entry. Shipping delete into three more styles on top of that would
have multiplied its blast radius, so the tile head moves onto commitPanelRemoval,
the same confirmed, ownership-proven, two-resource path the Dashboards tree uses. It
fails closed for a missing, shared, ambiguous, retargeted or non-panel query.

tests/e2e/dashboard-membership.spec.js asserted the bug as a browser contract
("the formerly owned copy has no owner now, so it joins the Library"). It is inverted,
and driven from Report — the style that previously had no delete — which is
#537's required browser test.

New and extracted modules

  • dashboard/application/panel-tile-actions.ts (new, 100% covered) — the pure
    menu vocabulary: labels, availability reasons, the removal question. Mirrors the
    tree's own dashboard-tree-model.ts split, keeping the branching out of
    ui/dashboard.ts.
  • panelRemovalRefusal — a dry run of removeDashboardPanel, so the Remove
    row is disabled with a reason instead of opening a confirmation that refuses at the
    end of itself (Dashboard tree: replace overflow menus with direct Panel focus, edit and delete actions (#429 phase 4) #494's rule). The two answers cannot drift: they are the same
    function.
  • ui/confirm-menu.ts (new) — the destructive-confirmation shape had reached
    three copies (dashboard-tree.ts, tabs.ts, and this), with drift already
    visible in three different Cancel focus behaviours. One of its decisions is
    invisible in the row order: the destructive row reads first, but Cancel holds
    initial focus (Dashboard-tree's orphaned-variable delete confirm autofocuses the destructive action, not Cancel #501) — exactly what a fourth hand-written caller gets wrong
    silently. Both existing callers move onto it with byte-identical DOM, so their
    230 tests pass untouched; that is the guardrail on the extraction.

Also here, because the change reaches them

  • openMenu now passes viewportW/panelW into its existing fixedAnchor call. A
    tile's is the first trigger at the viewport's right edge, where a
    left-aligned 252px panel ran off screen; Dashboard time-range control: token-based From/To picker in the filter bar #335's clamp was already written.
  • main-surface.ts gains withPendingFocus. The removal's own rebuild discards the
    card its successor focus is aimed at, and is not awaited — so the focus is owed
    to the next render rather than delivered when the commit resolves.
  • applyRouteCommand's remove-tile arm and the removeTileMembership import are
    deleted (the tile head was their only dispatcher). Two long-dead imports
    (queryDashboardRole, buildQueryOwnershipIndex) go with them. Without this
    src/ui/dashboard.ts drops below its 100% statement floor.
  • A flow KPI band member's overlay control is built once and moved into each
    repainted card. renderKpiInto replaces that card on every publish, including
    every refresh wave, and openMenu keys its registry on the trigger element — a
    rebuilt trigger would strand an open menu over a dead node.
  • The stays visible for its own menu's lifetime. aria-expanded covers the
    ordinary head; a grid KPI tile additionally needs a JS is-menu-open class,
    because that whole head is opacity: 0 and ancestor opacity composites.

Tests

Six unit tests that used the trash only as a lever to push a descriptor through
the optimistic command queue are re-pointed at update-placement, not at the new
removal: the atomic path never publishes optimistically, so "the tile is rendered
again" would have been true whether or not any rollback code existed. The #350
membership-restore test is rebuilt around an external producer adding a tile, for
the same reason — and sabotage-checked to confirm it still fails when the rebuild
trigger is removed.

Sabotage-checked: the container query, the Cancel autofocus, the focus restore, the
build-once band trigger, and the needsRebuild path.

npm test — 6575 tests, tsc --noEmit, boundaries, every per-file coverage floor.
Playwright green on chromium (180); the full three-engine run is the last gate.

Reconciliation

🤖 Generated with Claude Code

https://claude.ai/code/session_01Qjsxydw8WmdFFu2Pf8y9Dq

BorisTyshkevich and others added 4 commits July 29, 2026 11:46
…ery style

PR #538 (#535) left a Dashboard panel tile head carrying five controls — grip,
duplicate, widen, expand, delete — competing with the tile title for one flex
row. On a tile a single grid column wide there is no row left to compete for.

Edit mode is now `grip · title · widen · ⋯`; View mode is unchanged (its single
direct expand icon, no menu — one action does not need a menu). The ⋯ always
lists all four actions, and each row says why it cannot run when it cannot, so
nothing is ever merely absent. The inline widen is a shortcut on top of its row:
a CSS container query on the tile head withdraws it below ~260px of tile, where
the ellipsized title has nothing left to give.

Delete was gated TWICE to the grafana-grid engine — a `.dash-gg-grid` CSS
ancestor scope and an `activeEngine` check inside its own click handler — so
Report, 2 columns and 3 columns had no delete at all, and a flow KPI band member
had none under any style. An always-present menu row removes both gates.

Closes #537. The tile-head trash dispatched the document-only `remove-tile`
command, which leaves `workspace.queries` untouched; since #427 made every panel
tile the SOLE OWNER of a saved-query copy, that left the copy with zero owners —
which is exactly what makes a query a Library query — so a deleted panel came
back as an apparently standalone Library entry. It now takes the same confirmed,
ownership-proven, two-resource path the Dashboards tree uses, and fails closed
for a missing, shared, ambiguous, retargeted or non-panel query.

New pure module `dashboard/application/panel-tile-actions.ts` resolves the menu
vocabulary (labels, availability reasons, the removal question), mirroring the
tree's own `dashboard-tree-model.ts` split. `panelRemovalRefusal` is a DRY RUN of
`removeDashboardPanel`, so the row is disabled with a reason rather than opening
a confirmation that refuses at the end of itself.

`ui/confirm-menu.ts` extracts the destructive-confirmation shape, which had
reached THREE copies (dashboard-tree.ts, tabs.ts, and this) with drift already
visible in three different Cancel focus behaviours. One of its decisions is
invisible in the row order — the destructive row reads first but Cancel holds
initial focus (#501) — which is precisely what a fourth hand-written caller gets
wrong silently. Both existing callers move onto it with byte-identical DOM, so
their 230 tests pass untouched.

Also here, because the change reaches them:
- `openMenu` now passes `viewportW`/`panelW` into its existing `fixedAnchor`
  call. A tile's ⋯ is the first trigger at the viewport's RIGHT edge, where a
  left-aligned 252px panel ran off screen; #335's clamp was already written.
- `main-surface.ts` gains `withPendingFocus`. The removal's own rebuild discards
  the card its successor focus is aimed at, and is not awaited — so the focus is
  OWED to the next render rather than delivered when the commit resolves.
- `applyRouteCommand`'s `remove-tile` arm and the `removeTileMembership` import
  are deleted: the tile head was their only dispatcher, and the membership
  follow-up now happens inside `removeDashboardPanel`. Two long-dead imports
  (`queryDashboardRole`, `buildQueryOwnershipIndex`) go with them.
- A flow KPI band member's overlay control is built once and MOVED into each
  repainted card. `renderKpiInto` replaces that card on every publish, including
  every refresh wave, and `openMenu` keys its registry on the trigger element —
  a rebuilt trigger would strand an open menu over a dead node.

Six unit tests that used the trash only as a lever to push a descriptor through
the optimistic command queue are re-pointed at `update-placement`, not at the new
removal: the atomic path never publishes optimistically, so "the tile is rendered
again" would have been true whether or not any rollback code existed. The #350
membership-restore test is rebuilt around an external producer ADDING a tile, for
the same reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qjsxydw8WmdFFu2Pf8y9Dq
…he roadmap

The tile head's overflow menu partly reverses #494, which removed the ⋯ from every
Dashboards-TREE row, and supersedes #370's tile-removal semantics for panel-owned
queries. Four places asserted something that is now false and are corrected rather
than deleted: `styles.css`'s "gone from every production row", dashboard-tree.ts's
"There is no ⋯ any more", `Icon.more`'s comment (which described a tree-row menu it
had had no caller for since #494), and ADR-0003's "the band still has no delete
affordance".

The ADR gains a #544 addendum recording the three decisions this revises: why a
tile head may carry a ⋯ where a tree row may not, why an unavailable action is now
LISTED with a reason (inside a menu the row is the explanation — #452's argument —
whereas a bare icon has only a tooltip), and why a tile's delete stopped being a
layout command.

Also files what the work surfaced rather than burying it: #542 (a tile whose query
is missing or shared is now unremovable from any surface, which #537's fail-closed
rule requires), #543 (a two-resource write drops the transient Full view —
pre-existing since #538's duplicate, and asserted as-is in a test so it fails
loudly when fixed), and #498 re-scoped with the five further class names the
typography gate cannot see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qjsxydw8WmdFFu2Pf8y9Dq
…ow-menu-537

# Conflicts:
#	CHANGELOG.md
#	docs/ADR-0003-dashboard-viewing.md
#	src/ui/tabs.ts
…ow-menu-537

# Conflicts:
#	CHANGELOG.md
#	src/ui/dashboard-tree.ts
@BorisTyshkevich
BorisTyshkevich merged commit 893ff03 into main Jul 29, 2026
7 checks passed
@BorisTyshkevich
BorisTyshkevich deleted the feat/tile-head-overflow-menu-537 branch August 6, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tile-header delete should remove the panel’s owned query atomically

1 participant