feat(#544): collapse the tile head into a ⋯ menu, atomic delete in every style (closes #537) - #545
Merged
Merged
Conversation
…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
Closed
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 asingle grid column wide (~100px at twelve columns) has no row left to compete for.
grip · title · widen · ⋯The
⋯always lists all four actions, in the design's order, with remove lastbehind a separator: Duplicate panel, Widen (carrying
widenLabel's dynamicdestination 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: noneand themenu still offers "Widen to 2 columns".
The consistency fix
Delete was gated twice to the grafana-grid engine — a
.dash-gg-gridCSSancestor scope and an
activeEngine === 'grafana-grid'check inside its own clickhandler — 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-tilecommand, whichleaves
workspace.queriesuntouched. Since #427 makes every panel tile the soleowner 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.jsasserted 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 puremenu vocabulary: labels, availability reasons, the removal question. Mirrors the
tree's own
dashboard-tree-model.tssplit, keeping the branching out ofui/dashboard.ts.panelRemovalRefusal— a dry run ofremoveDashboardPanel, so the Removerow 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 reachedthree copies (
dashboard-tree.ts,tabs.ts, and this), with drift alreadyvisible 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
openMenunow passesviewportW/panelWinto its existingfixedAnchorcall. Atile's
⋯is the first trigger at the viewport's right edge, where aleft-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.tsgainswithPendingFocus. The removal's own rebuild discards thecard 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'sremove-tilearm and theremoveTileMembershipimport aredeleted (the tile head was their only dispatcher). Two long-dead imports
(
queryDashboardRole,buildQueryOwnershipIndex) go with them. Without thissrc/ui/dashboard.tsdrops below its 100% statement floor.repainted card.
renderKpiIntoreplaces that card on every publish, includingevery refresh wave, and
openMenukeys its registry on the trigger element — arebuilt trigger would strand an open menu over a dead node.
⋯stays visible for its own menu's lifetime.aria-expandedcovers theordinary head; a grid KPI tile additionally needs a JS
is-menu-openclass,because that whole head is
opacity: 0and 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 newremoval: 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
needsRebuildpath.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
two-control row in a fixed-width side pane — and is not reopened. Recorded in an
ADR-0003 addendum,
CHANGELOG.md, and roadmap Roadmap to 1.0.0 #68, with the four comments thatasserted "the
⋯is gone from every production row" corrected rather than deleted.membership normalisation and
spec.favoritemirroring it defined are unchanged andrun inside the atomic transform.
unremovable from any surface — Tile-header delete should remove the panel’s owned query atomically #537 requires fail-closed, and this removes the last
tile-only escape hatch) and A two-resource write (duplicate / remove panel) silently drops Full view #543 (a two-resource write drops the transient Full
view; pre-existing since feat(#535): duplicate, widen and expand actions on a panel tile head #538's duplicate, now asserted as-is so it fails loudly
when fixed). Typography contract misses classes supplied through openMenu options #498 re-scoped with the five further class names the typography
gate cannot see.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Qjsxydw8WmdFFu2Pf8y9Dq