feat(#291): grafana-grid@1 — rowless Grafana-style tile-grid Dashboard layout#308
Merged
Conversation
…istration (wave 1)
- schemas/dashboard-layout-grafana-grid-v1.schema.json: rowless 12-col engine,
items {span 1..12, height compact|medium|large}, no preset; manifest entry +
regenerated types/validators (fallback slot stays pinned to flow@1)
- src/dashboard/layouts/grafana-grid-layout.ts: DashboardLayoutPlugin +
computeGrafanaGridLayout (deterministic row-major packing, responsive column
clamp 12/6/4/2), deriveGrafanaGridPlacement, flow<->grid span conversion,
deriveFlowFallback (validated flow@1)
- workspace-semantics: isSupportedLayout generalized to the engine map; new
narrow isFlowLayout for fallback/flow-only call sites so a grafana-grid doc
can never be accepted as a flow fallback; semantics validation now validates
a non-flow primary against its own schema AND still requires a flow@1 fallback
- defaultLayoutRegistry registers grafana-grid@1 behind a lazy load()
- tests: grafana-grid-layout (100/100/100/100), layout-registry, workspace-
semantics, schema-build extended
Part of #291.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz
- change-layout switches engines: flow->grid seeds items from flow placements (span 1->4, 2->6, 3->12; height carried) and snapshots the flow layout as fallback; grid->flow restores the fallback (optionally applying a preset); missing fallback -> dashboard-command-layout-fallback-missing diagnostic - update-placement validates/writes through the ACTIVE engine plugin (grid span 1..12, flow 1..3) via new resolveLayoutPluginSync - add-time seeding under grid uses deriveGrafanaGridPlacement (always explicit) - regenerateGridFallback shared primitive: every tile/placement mutation under grid (commands, tile-membership, saved-query-mutation) deterministically regenerates a valid flow@1 fallback - UI never manages fallback itself - tests: commands engine-switch matrix, fallback regen per mutating command, per-engine placement bounds, membership/mutation grid cases Part of #291. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz
…t interactions (wave 3)
- dashboard-viewer-session: layout routed through the registry-resolved active
plugin; DashboardViewState.layout becomes a discriminated union
({engine:'flow', ...FlowLayoutModel} | {engine:'grafana-grid', grid});
containerWidth dep feeds the responsive column clamp (12/6/4/2)
- src/ui/dashboard.ts: single-CSS-grid reconciliation path (.dash-gg-grid,
8px gap, span + semantic-height classes 118/210/296px), KPI tiles placed
like any tile; layout select extended to 4 flow presets + Grafana grid
driving engine-switching change-layout; edit mode (!readOnly): whole-card
drag-reorder (move-tile), corner-drag resize with pure snap math ->
one update-placement per drag, tile delete (remove-tile); immediate
propagation - no Save/Undo (owner decision)
- fixed pre-existing runCommand bug: plugin was hardcoded to flow for
validation + normalize; now resolves the active engine (post-command
layout for normalize, so engine switches normalize through the result)
- grafana-grid-layout: GRID_GAP_PX/GRID_HEIGHT_PX + snapGridSpan/
snapGridHeight pure resize math (100% covered)
- styles.css: grid host/tile/edit-affordance chrome, both themes
Part of #291.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz
…e 3b) Narrow static harness (dashboard-mobile.html pattern, no createApp): packing positions vs the pure model, semantic height px, container-width column clamp 12/6/4/2, no horizontal overflow at 360px, corner-drag resize live preview + single terminal dispatch, hover-revealed chrome + view-mode absence. 6/6 green on chromium and webkit (firefox full-parallel goto flake is pre-existing; spec passes 6/6 at --workers=1). Part of #291. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz
… cleanup Verified findings from the 8-angle review + adversarial verification: - remove the 768px !important grid-template override that fought the JS container-width clamp (span>1 tiles rendered on implicit phantom tracks in the 470-768px band; reproduced in real chromium) - measure the grid's CONTENT-box width (clientWidth minus computed padding) for both breakpoint tiers and resize column math (pure contentBoxWidth) - corner-drag resize pins the tile to its explicit colStart for the drag (span clamped to remaining columns) so a mid-row tile can never self-wrap and desync the persisted placement from the pointer - renderDashboard removes the previously-installed window resize listener (reloadDashboardRoute re-entry stacked handlers + pinned stale sessions) - bare same-engine change-layout (no items) preserves existing placements instead of silently wiping them via the wholesale branch - one BUILTIN_SYNC_PLUGINS table now feeds both resolveLayoutPluginSync and defaultLayoutRegistry (single registration point for engine #3) - delete dead resolveActiveLayoutPlugin (stale semantics, zero prod callers); drop redundant GrafanaGridLayoutModel.order; regenerateGridFallback owns the type guard + tile-ref mapping (3 call sites collapsed, no flow-path alloc); setPlacementForActiveEngine dedupes the per-engine write branch - e2e harness updated to the pinned-resize contract + new mid-row drag test Gates: npm test 3657 passed, build OK, check:arch OK, full e2e chromium+webkit 104 passed. Part of #291. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz
Firefox does not deliver real mouse moves ~100000px outside the viewport, so the extreme rightward drag never reached the wiring and the span stayed at 1. Dispatch that single extreme pointermove synthetically on window (where the resize wiring listens); real mouse input still drives down/pin/up and the terminal dispatch. 21/21 green locally on chromium+firefox+webkit at --workers=1. Part of #291. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz
Owner override on the served build: the corner-drag vertical range was capped at the 3-tier 296px max. grafana-grid item height is now an integer 1..16 in row units (px = 32 + 88*units; units 1/2/3 land on the legacy tiers, unit 16 = 1440px ~5x the old cap). The schema accepts the legacy compact|medium|large strings (anyOf) and normalize() upgrades them to 1/2/3; flow fallback maps units 1->compact, 2->medium, >=3->large; the resize snap is the exact inverse of the px formula (per-unit stops). Height tier classes replaced by a computed inline height; e2e heights scenario now covers units 1/2/3/10 (120/208/296/912px). Gates: npm test 3673 passed, build OK, check:arch OK, grid+mobile e2e 30 passed chromium+webkit, grid spec 7 passed firefox --workers=1. Part of #291. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz
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.
What & why
Closes #291. Adds
grafana-grid@1— a rowless Grafana-style 12-column tile-grid Dashboard layout engine next to the normativeflow@1, building on the just-merged Dashboard v1 (#280, PRs #293/#301/#304/#306). Visual reference: theGrafana Dashboard.htmlmockup in the "sql browser" design project (rows/folding explicitly excluded — owner decision).Owner decisions (question round, recorded in the issue body): span+height+order placement model (no x/y); corner-drag resize per the mockup; engine switching via the existing layout select; layout-engine-only scope (no time-range/auto-refresh); immediate propagation — no Save-layout/Undo; modes follow #306/ADR-0003 (no in-page toggle).
schemas/dashboard-layout-grafana-grid-v1.schema.json(items: {span: 1–12, height: compact|medium|large}) + generated types/validators; pure pluginsrc/dashboard/layouts/grafana-grid-layout.ts(deterministic row-major packing from canonicaltiles[]order, content-box container-width column clamp 12/6/4/2 at ≥1160/≥720/≥470 px, flow↔grid span conversion, sizeHints seeding, pure resize-snap math). Registered registration-time-lazily in the layout registry from a singleBUILTIN_SYNC_PLUGINStable;isSupportedLayoutgeneralized with a narrowisFlowLayoutso thefallbackslot stays pinned toflow@1.change-layout(flow→grid seeds spans 1→4/2→6/3→12 and snapshots the flow layout asfallback; grid→flow restores it; bare same-engine re-dispatch preserves placements),update-placementvalidated by the active engine, and every grid mutation deterministically regenerates a validflow@1fallback (spans 1–4→1/5–8→2/9–12→3) via one shared primitive across commands/tile-membership/saved-query-mutation. Fixed a pre-existing bug:runCommandhardcoded the flow plugin for validation+normalize.DashboardViewState.layoutbecomes a discriminated union (flowbit-identical + additive tag); a second reconciliation path renders a single CSS grid (8 px gap, semantic heights 118/210/296 px; KPI tiles place like any tile). Edit mode (!readOnlyonly): whole-card drag-reorder, corner-drag resize (span snaps to columns, height to tiers; tile pinned to its column during the drag so the persisted placement always matches the pointer), tile delete. Layout select: 4 flow presets + Grafana grid.!importantCSS rule that fought the JS clamp (real-chromium repro: phantom implicit tracks in the 470–768 px band), content-box width measurement, resize-listener teardown on route re-entry, same-engine data-loss guard, single sync-plugin dispatch table, deadresolveActiveLayoutPluginremoved, redundant view-model field dropped, tile-refs mapping centralized.tests/e2e/dashboard-grid.*(packing positions vs the pure model, semantic heights, responsive clamp, 360 px no-overflow, mid-row corner-drag resize, hover chrome) — 6/6 chromium+webkit; full e2e suite 104 passed on chromium+webkit (firefox full-parallelpage.gotoflake is pre-existing/environmental; the spec passes 6/6 on firefox at--workers=1).Deliberately deferred (issue body §Reconciled): time-range control + auto-refresh (need their own contract design); conversion orchestration stays in
change-layoutwhile there are two engines.Checklist
npm testpasses (the per-file coverage gate is non-negotiable)npm run buildsucceeds (single-filedist/sql.html)src/core/, network insrc/net/(injected fetch), DOM insrc/ui/CHANGELOG.md([Unreleased]) updated if behavior or the deployed surface changed🤖 Generated with Claude Code
https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz