Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,34 @@ auto-generated per-PR notes; this file is the curated, human-readable history.
## [Unreleased]

### Added
- **`grafana-grid@1` — a rowless Grafana-style tile-grid Dashboard layout**
(#291, building on #280). A second layout engine next to the normative
`flow@1`: a responsive 12-column tile grid with deterministic packing —
per-tile `{span: 1–12, height: 1–16 row units}` (`px = 32 + 88×units`;
units 1/2/3 ≈ the flow tiers, unit 16 = 1440 px ≈ 5× flow's tallest;
the legacy `compact|medium|large` strings stay readable and upgrade to
1/2/3 on the next edit) persisted in `layout.items`, vertical position
derived purely from the
canonical `dashboard.tiles[]` order, and a container-width column clamp
(12/6/4/2 at ≥1160/≥720/≥470 px of *content-box* width) that never mutates
persisted spans. No rows, no folding (owner decision). The engine registers
lazily in the layout registry (`schemas/dashboard-layout-grafana-grid-v1.
schema.json` + generated types/validator; the `fallback` slot stays pinned
to `flow@1`), and every grid mutation deterministically regenerates a
**valid `flow@1` fallback** (grid→flow span 1–4→1, 5–8→2, 9–12→3). The
edit-mode layout select gains a **Grafana grid** option: switching from
flow seeds spans from the current flow placements (1→4, 2→6, 3→12) and
snapshots the flow layout as the fallback; switching back restores it.
Workbench-only (edit-mode) placement editing per the mock: whole-card
drag-reorder, **corner-drag resize** (span snaps to columns, height snaps
per row unit across all 16 stops; the tile is pinned to its column during
the drag so the persisted placement always matches the pointer), and tile
delete — every change propagates immediately (no Save/Undo, owner
decision). KPI tiles place like any other tile (no KPI band). Filters,
panel/KPI rendering, and view-mode read-only rules (#306/ADR-0003) are
reused unchanged. Includes a real-browser e2e harness
(`tests/e2e/dashboard-grid.*`) covering packing, row-unit heights, the
responsive clamp, 360 px overflow, resize, and hover chrome.
- **Direct + full-screen Dashboard viewing with a one-time cross-tab handoff**
(#288, Dashboard v1 Phase 6 — the final phase of #280; also lands #302). Two
explicit viewing modes on the standalone `/dashboard` route (ADR-0003): an
Expand Down
13 changes: 13 additions & 0 deletions build/schema-manifest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ export const SCHEMA_MANIFEST = [
validatorExport: 'validateFlowLayoutV1',
typeExport: 'FlowLayoutV1',
},
// grafana-grid@1 (#291): a second layout engine, sibling to flow@1. The
// generic layout envelope in dashboard-v1.schema.json is already open
// (type/version/items are unconstrained there), so this schema needs no
// $ref from dashboard-v1 — it is its own manifest root purely to get a
// compiled validator + generated types, exactly like flow's own root. The
// `fallback` slot stays pinned to flow@1 only; this schema is never a
// fallback target.
{
path: 'schemas/dashboard-layout-grafana-grid-v1.schema.json',
schemaExport: 'grafanaGridLayoutV1Schema',
validatorExport: 'validateGrafanaGridLayoutV1',
typeExport: 'GrafanaGridLayoutV1',
},
{
path: 'schemas/dashboard-v1.schema.json',
schemaExport: 'dashboardV1Schema',
Expand Down
61 changes: 61 additions & 0 deletions schemas/dashboard-layout-grafana-grid-v1.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://altinity.com/schemas/altinity-sql-browser/dashboard-layout-grafana-grid-v1.schema.json",
"title": "Altinity SQL Browser Dashboard grafana-grid@1 layout",
"description": "The normative grafana-grid@1 Dashboard layout: rowless deterministic packing driven by dashboard.tiles order into a single 12-column grid, and per-tile span/height placements keyed by tile ID. Unlike flow@1 this engine has no preset. A grafana-grid primary layout always pairs with a valid flow@1 DashboardLayoutFallbackV1 document, regenerated on every mutation.",
"x-altinity-kind": "dashboard-layout-grafana-grid",
"x-altinity-version": 1,
"type": "object",
"required": ["type", "version", "items"],
"properties": {
"type": {
"title": "Layout engine",
"description": "Layout engine identifier; always grafana-grid for this contract.",
"type": "string",
"const": "grafana-grid"
},
"version": {
"title": "Layout engine version",
"description": "grafana-grid contract version; always 1 for this contract.",
"type": "integer",
"const": 1
},
"items": {
"title": "Tile placements",
"description": "Per-tile placement keyed by tile ID. A missing placement uses span 6 and medium height.",
"type": "object",
"maxProperties": 100,
"propertyNames": { "minLength": 1, "maxLength": 256 },
"additionalProperties": { "$ref": "#/$defs/grafanaGridTilePlacementV1" }
}
},
"additionalProperties": false,
"x-altinity-order": ["type", "version", "items"],
"$defs": {
"grafanaGridHeightV1": {
"title": "Tile height",
"description": "Tile height in numeric row units (1..16); px = 32 + 88*units, so units 1/2/3 land close to the legacy compact/medium/large tiers (120/208/296px) and unit 16 reaches 1440px. The legacy compact|medium|large strings are still accepted for backward compatibility and are normalized to their numeric equivalents (1/2/3) by the layout's `normalize` step; new writes should always be numeric.",
"anyOf": [
{ "type": "integer", "minimum": 1, "maximum": 16 },
{ "type": "string", "enum": ["compact", "medium", "large"] }
]
},
"grafanaGridTilePlacementV1": {
"title": "Tile placement",
"description": "Closed placement contract: unknown fields fail validation. Future extension requires grafana-grid@2 or an explicit extension namespace.",
"type": "object",
"properties": {
"span": {
"title": "Column span",
"description": "Columns (of 12) the tile occupies; the effective span is clamped to the active column count at each responsive breakpoint.",
"type": "integer",
"minimum": 1,
"maximum": 12
},
"height": { "$ref": "#/$defs/grafanaGridHeightV1" }
},
"additionalProperties": false,
"x-altinity-order": ["span", "height"]
}
}
}
90 changes: 90 additions & 0 deletions schemas/generated/library-v2.bundle.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,96 @@
}
}
},
"dashboard-layout-grafana-grid-v1": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://altinity.com/schemas/altinity-sql-browser/dashboard-layout-grafana-grid-v1.schema.json",
"title": "Altinity SQL Browser Dashboard grafana-grid@1 layout",
"description": "The normative grafana-grid@1 Dashboard layout: rowless deterministic packing driven by dashboard.tiles order into a single 12-column grid, and per-tile span/height placements keyed by tile ID. Unlike flow@1 this engine has no preset. A grafana-grid primary layout always pairs with a valid flow@1 DashboardLayoutFallbackV1 document, regenerated on every mutation.",
"x-altinity-kind": "dashboard-layout-grafana-grid",
"x-altinity-version": 1,
"type": "object",
"required": [
"type",
"version",
"items"
],
"properties": {
"type": {
"title": "Layout engine",
"description": "Layout engine identifier; always grafana-grid for this contract.",
"type": "string",
"const": "grafana-grid"
},
"version": {
"title": "Layout engine version",
"description": "grafana-grid contract version; always 1 for this contract.",
"type": "integer",
"const": 1
},
"items": {
"title": "Tile placements",
"description": "Per-tile placement keyed by tile ID. A missing placement uses span 6 and medium height.",
"type": "object",
"maxProperties": 100,
"propertyNames": {
"minLength": 1,
"maxLength": 256
},
"additionalProperties": {
"$ref": "#/$defs/grafanaGridTilePlacementV1"
}
}
},
"additionalProperties": false,
"x-altinity-order": [
"type",
"version",
"items"
],
"$defs": {
"grafanaGridHeightV1": {
"title": "Tile height",
"description": "Tile height in numeric row units (1..16); px = 32 + 88*units, so units 1/2/3 land close to the legacy compact/medium/large tiers (120/208/296px) and unit 16 reaches 1440px. The legacy compact|medium|large strings are still accepted for backward compatibility and are normalized to their numeric equivalents (1/2/3) by the layout's `normalize` step; new writes should always be numeric.",
"anyOf": [
{
"type": "integer",
"minimum": 1,
"maximum": 16
},
{
"type": "string",
"enum": [
"compact",
"medium",
"large"
]
}
]
},
"grafanaGridTilePlacementV1": {
"title": "Tile placement",
"description": "Closed placement contract: unknown fields fail validation. Future extension requires grafana-grid@2 or an explicit extension namespace.",
"type": "object",
"properties": {
"span": {
"title": "Column span",
"description": "Columns (of 12) the tile occupies; the effective span is clamped to the active column count at each responsive breakpoint.",
"type": "integer",
"minimum": 1,
"maximum": 12
},
"height": {
"$ref": "#/$defs/grafanaGridHeightV1"
}
},
"additionalProperties": false,
"x-altinity-order": [
"span",
"height"
]
}
}
},
"dashboard-v1": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://altinity.com/schemas/altinity-sql-browser/dashboard-v1.schema.json",
Expand Down
6 changes: 6 additions & 0 deletions schemas/generated/schema-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
"id": "https://altinity.com/schemas/altinity-sql-browser/dashboard-layout-flow-v1.schema.json",
"path": "../dashboard-layout-flow-v1.schema.json"
},
{
"kind": "dashboard-layout-grafana-grid",
"version": 1,
"id": "https://altinity.com/schemas/altinity-sql-browser/dashboard-layout-grafana-grid-v1.schema.json",
"path": "../dashboard-layout-grafana-grid-v1.schema.json"
},
{
"kind": "dashboard",
"version": 1,
Expand Down
16 changes: 10 additions & 6 deletions src/dashboard/application/dashboard-authoring-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { diagnostic, sortDiagnostics } from '../model/workspace-diagnostics.js';
import type { WorkspaceDiagnostic } from '../model/workspace-diagnostics.js';
import { resolveDashboardPresentations } from '../model/presentation-resolver.js';
import { buildDashboardExportBundle } from '../model/dashboard-export.js';
import { resolveActiveLayoutPlugin } from '../layouts/flow-layout.js';
import { defaultLayoutRegistry } from '../layouts/layout-registry.js';
import { applyCommand } from './dashboard-commands.js';
import type { DashboardCommand, DashboardCommandResult } from './dashboard-commands.js';
import { createQueryResolver } from './dashboard-query-resolver.js';
Expand Down Expand Up @@ -145,16 +145,20 @@ export function createDashboardAuthoringSession(
`Command expected draft version ${options.expectedDraftVersion} but the draft is at ${baseVersion}`)], baseVersion);
}

// Resolve the active layout plugin — for change-layout, the NEW layout.
// Resolve the active layout plugin through the full registry (#291: the
// grafana-grid@1 engine, not just flow@1) — for change-layout, the NEW
// layout; for every other command, the document's CURRENTLY active
// layout, so `update-placement`/`add-query` seeding and validation route
// through whichever engine (grid or flow) is actually active.
const layoutForPlugin = command.type === 'change-layout' ? command.layout : current.document.layout;
const load = resolveActiveLayoutPlugin(layoutForPlugin);
if (!load.ok) return returnFail<T>(load.diagnostics, baseVersion);
const resolved = await defaultLayoutRegistry.resolve(layoutForPlugin);
if (!resolved.ok) return returnFail<T>(resolved.diagnostics, baseVersion);

const resolver = createQueryResolver(queries);
const applied = applyCommand(current.document, command, { resolver, genTileId: genId, plugin: load.plugin });
const applied = applyCommand(current.document, command, { resolver, genTileId: genId, plugin: resolved.plugin });
if (!applied.ok) return returnFail<T>(applied.diagnostics, baseVersion);

const normalized = load.plugin.normalize(applied.dashboard);
const normalized = resolved.plugin.normalize(applied.dashboard);
const diagnostics = validateCandidate(normalized);
if (diagnostics.length) return returnFail<T>(diagnostics, baseVersion);

Expand Down
Loading
Loading