Skip to content

feat(#291): grafana-grid@1 — rowless Grafana-style tile-grid Dashboard layout#308

Merged
BorisTyshkevich merged 8 commits into
mainfrom
feat/grafana-grid-291
Jul 18, 2026
Merged

feat(#291): grafana-grid@1 — rowless Grafana-style tile-grid Dashboard layout#308
BorisTyshkevich merged 8 commits into
mainfrom
feat/grafana-grid-291

Conversation

@BorisTyshkevich

Copy link
Copy Markdown
Collaborator

What & why

Closes #291. Adds grafana-grid@1 — a rowless Grafana-style 12-column tile-grid Dashboard layout engine next to the normative flow@1, building on the just-merged Dashboard v1 (#280, PRs #293/#301/#304/#306). Visual reference: the Grafana Dashboard.html mockup 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).

  • Schema/core: schemas/dashboard-layout-grafana-grid-v1.schema.json (items: {span: 1–12, height: compact|medium|large}) + generated types/validators; pure plugin src/dashboard/layouts/grafana-grid-layout.ts (deterministic row-major packing from canonical tiles[] 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 single BUILTIN_SYNC_PLUGINS table; isSupportedLayout generalized with a narrow isFlowLayout so the fallback slot stays pinned to flow@1.
  • Application: engine-aware change-layout (flow→grid seeds spans 1→4/2→6/3→12 and snapshots the flow layout as fallback; grid→flow restores it; bare same-engine re-dispatch preserves placements), update-placement validated by the active engine, and every grid mutation deterministically regenerates a valid flow@1 fallback (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: runCommand hardcoded the flow plugin for validation+normalize.
  • UI: DashboardViewState.layout becomes a discriminated union (flow bit-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 (!readOnly only): 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.
  • Review round (8 finder angles + adversarial verification, all fixes applied): removed a 768 px !important CSS 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, dead resolveActiveLayoutPlugin removed, redundant view-model field dropped, tile-refs mapping centralized.
  • Tests: unit suites for plugin/commands/viewer-session/UI at the per-file gate (3657 tests green) + a new real-browser e2e harness 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-parallel page.goto flake 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-layout while there are two engines.

Checklist

  • npm test passes (the per-file coverage gate is non-negotiable)
  • Tests added/updated in the same change as the code
  • npm run build succeeds (single-file dist/sql.html)
  • Layers kept honest: pure logic in src/core/, network in src/net/ (injected fetch), DOM in src/ui/
  • No new runtime dependency (or it's a deliberate, justified addition — see CONTRIBUTING)
  • README / CHANGELOG.md ([Unreleased]) updated if behavior or the deployed surface changed
  • Reconciled affected tracked work (roadmap Roadmap to 1.0.0 #68, the issue body, ADR/CHANGELOG) if this change reshaped it

🤖 Generated with Claude Code

https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz

BorisTyshkevich and others added 8 commits July 18, 2026 17:34
…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
@BorisTyshkevich
BorisTyshkevich merged commit 333210e into main Jul 18, 2026
9 checks passed
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.

Add a rowless Grafana-style tile-grid Dashboard layout

1 participant