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
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,36 @@ auto-generated per-PR notes; this file is the curated, human-readable history.
## [Unreleased]

### Changed
- **Grid Tiles is the default Dashboard style; Full view replaces the old
Full width preset** (#321). The `grafana-grid@1` engine is renamed to
**Grid Tiles** in the UI (the persisted `{type:'grafana-grid',version:1}`
identifier is unchanged) and is now the layout every newly created
Dashboard starts in (empty `items`, a `columns-2` flow fallback; new tiles
keep the span-6/height-2 default). The old `flow@1/full-width` preset is
removed completely — from the flow JSON Schema and generated types, flow
normalization/render, the selector, `is-wide` CSS, legacy layout mapping
(`wide` now maps to `report`), grid→flow fallback generation (now
`columns-2`), and all fixtures/tests; valid flow presets are now only
`report`, `columns-2`, `columns-3`. **Full view** is introduced as a
transient, never-persisted render mode over Grid Tiles: every tile renders
one-per-row at the full effective column count (12/6/4/2 responsive) while
the authored spans are left untouched. Toggling Grid Tiles ↔ Full view runs
no `change-layout`, never commits, and never bumps the Dashboard revision;
a reload or a newly opened viewer session always starts in Grid Tiles.
Selecting Full view from a flow preset performs exactly one persisted
flow→grid conversion, then only the transient override; selecting a flow
preset from Full view clears the override and persists the flow change.
While Full view is active, reorder/add/delete and vertical height changes
still persist, but the corner resize becomes **vertical-only** (a
`ns-resize` affordance with a "Resize tile height" label) — horizontal
pointer movement can never change a span. The editable style selector is
ordered `Grid Tiles, Full view, Report, 2 columns, 3 columns`; a read-only
Dashboard exposes a reduced `Grid Tiles / Full view` runtime toggle (only
when its layout is grafana-grid — a read-only flow Dashboard shows no
selector), and the selector's accessible name is now `Dashboard style`.
Export/import never carry Full view state. There is no read-compatibility
path for `flow@1/full-width` development data (owner decision — the project
has no production compatibility requirement for it).
- **Grafana-grid KPI tiles are polished in both Dashboard modes** (#316,
follow-on to #291). Edit mode keeps the full editing shell but drops the
never-populated tile footer (no more phantom separator line). View mode
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Panel preview and an unconfigured Dashboard KPI tile show the cards inside the
ordinary `.kpi-panel` grid, while a **favorited, explicitly-KPI-typed** Dashboard
query instead joins a full-width **KPI band** — a flat, wrapping card stream
with no per-favorite name, description, or statistics footer, spanning every
Dashboard layout (Full width/Report/2/3 columns). Consecutive explicit KPI
flow Dashboard layout (Report/2/3 columns). Consecutive explicit KPI
favorites merge into one shared band. The complete
[`kpi-panel.json`](examples/kpi-panel.json) Library example can be opened from
**File ▾ → Import queries** to see both.
Expand Down
4 changes: 2 additions & 2 deletions schemas/dashboard-layout-flow-v1.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@
"$defs": {
"flowPresetV1": {
"title": "Flow preset",
"description": "Desktop column arrangement: full-width and report render one column (report centers a constrained-width column), columns-2 and columns-3 render equal columns.",
"description": "Desktop column arrangement: report renders one constrained-width centered column, columns-2 and columns-3 render equal columns.",
"type": "string",
"enum": ["full-width", "report", "columns-2", "columns-3"]
"enum": ["report", "columns-2", "columns-3"]
},
"flowHeightV1": {
"title": "Tile height",
Expand Down
3 changes: 1 addition & 2 deletions schemas/generated/library-v2.bundle.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1252,10 +1252,9 @@
"$defs": {
"flowPresetV1": {
"title": "Flow preset",
"description": "Desktop column arrangement: full-width and report render one column (report centers a constrained-width column), columns-2 and columns-3 render equal columns.",
"description": "Desktop column arrangement: report renders one constrained-width centered column, columns-2 and columns-3 render equal columns.",
"type": "string",
"enum": [
"full-width",
"report",
"columns-2",
"columns-3"
Expand Down
8 changes: 7 additions & 1 deletion src/dashboard/application/dashboard-authoring-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type { WorkspaceDiagnostic } from '../model/workspace-diagnostics.js';
import { resolveDashboardPresentations } from '../model/presentation-resolver.js';
import { buildDashboardExportBundle } from '../model/dashboard-export.js';
import { defaultLayoutRegistry } from '../layouts/layout-registry.js';
import { deriveFlowFallback } from '../layouts/grafana-grid-layout.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 @@ -86,7 +87,12 @@ export interface DashboardAuthoringSessionDeps {
function createEmptyDashboard(id: string): DashboardDocumentV1 {
return {
documentVersion: 1, id, title: 'Dashboard', revision: 1,
layout: { type: 'flow', version: 1, preset: 'full-width', items: {} },
layout: {
type: 'grafana-grid',
version: 1,
items: {},
fallback: deriveFlowFallback({ type: 'grafana-grid', version: 1, items: {} }, []),
},
filters: [], tiles: [],
};
}
Expand Down
29 changes: 25 additions & 4 deletions src/dashboard/application/dashboard-viewer-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import type { WorkspaceDiagnostic } from '../model/workspace-diagnostics.js';
import { computeFlowLayout } from '../layouts/flow-layout.js';
import type { FlowLayoutModel } from '../layouts/flow-layout.js';
import { computeGrafanaGridLayout } from '../layouts/grafana-grid-layout.js';
import type { GrafanaGridLayoutModel } from '../layouts/grafana-grid-layout.js';
import type { GrafanaGridLayoutModel, GridRenderMode } from '../layouts/grafana-grid-layout.js';
import { resolveLayoutPluginSync } from '../layouts/layout-registry.js';
import type { DashboardLayoutRegistry } from '../layouts/layout-registry.js';
import type {
Expand Down Expand Up @@ -109,7 +109,7 @@ export interface ViewerFilterState {
* same-named fields (both have `columns`) never collide on one object. */
export type DashboardLayoutView =
| (FlowLayoutModel & { engine: 'flow' })
| { engine: 'grafana-grid'; grid: GrafanaGridLayoutModel };
| { engine: 'grafana-grid'; grid: GrafanaGridLayoutModel; renderMode: GridRenderMode };

export interface DashboardViewState {
tiles: ViewerTileState[];
Expand Down Expand Up @@ -220,6 +220,15 @@ export interface DashboardViewerSession {
* flow model is recomputed. The tile SET must be unchanged (a membership
* change rebuilds the session). */
syncDocument(next: DashboardDocumentV1): void;
/** #321 "Full view": set the TRANSIENT grafana-grid render-mode override
* ('tiles' = today's packed multi-tile-per-row grid, 'full' = every tile
* full-width, one per row). Runtime-only — never persisted, never a
* document mutation, never a commit/revision bump; it just republishes the
* current document through the new mode. Survives every other command
* (add/remove/reorder/height/syncDocument) since it lives outside
* `documentRef` entirely. A fresh session (reload/new viewer) always starts
* at 'tiles'. */
setGridRenderMode(mode: GridRenderMode): void;
/** Cancel all work and turn every later entry point into a no-op. */
destroy(): void;
}
Expand Down Expand Up @@ -297,6 +306,10 @@ export function createDashboardViewerSession(deps: DashboardViewerDeps): Dashboa
// re-running tiles; the initial tile SET is fixed for the session's analysis.
let documentRef: DashboardDocumentV1 = deps.document;
let destroyed = false;
// #321 "Full view": a TRANSIENT runtime render-mode override, entirely
// outside `documentRef` — never read/written by any command, never
// persisted. A fresh session always starts at 'tiles'.
let gridRenderMode: GridRenderMode = 'tiles';

const queryById = new Map<string, SavedQueryV2>();
for (const query of queries) {
Expand Down Expand Up @@ -429,8 +442,9 @@ export function createDashboardViewerSession(deps: DashboardViewerDeps): Dashboa
? {
engine: 'grafana-grid',
grid: computeGrafanaGridLayout({
tiles: visible, layout: documentRef.layout, containerWidth: deps.containerWidth?.(),
tiles: visible, layout: documentRef.layout, containerWidth: deps.containerWidth?.(), renderMode: gridRenderMode,
}),
renderMode: gridRenderMode,
}
: { engine: 'flow', ...computeFlowLayout({ tiles: visible, layout: documentRef.layout, mobile }) };
return {
Expand Down Expand Up @@ -747,6 +761,12 @@ export function createDashboardViewerSession(deps: DashboardViewerDeps): Dashboa
publish();
}

function setGridRenderMode(mode: GridRenderMode): void {
if (destroyed || gridRenderMode === mode) return;
gridRenderMode = mode;
publish();
}

function destroy(): void {
destroyed = true;
for (const runtime of tiles) {
Expand All @@ -762,6 +782,7 @@ export function createDashboardViewerSession(deps: DashboardViewerDeps): Dashboa
return {
state: stateSignal as ReadonlySignal<DashboardViewState>,
controls, getFilterField,
start, refresh, refreshTile, setFilter, applyFilter, clearFilter, clearAllFilters, cancelTile, syncDocument, destroy,
start, refresh, refreshTile, setFilter, applyFilter, clearFilter, clearAllFilters, cancelTile, syncDocument,
setGridRenderMode, destroy,
};
}
11 changes: 6 additions & 5 deletions src/dashboard/layouts/flow-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,11 @@ export const flowLayoutPlugin: DashboardLayoutPlugin = {
// stay renderer/theme concerns; this module owns column count, effective span,
// deterministic row-major packing, KPI-band grouping, and mobile normalization.

/** Desktop column count for each flow preset (#280 "Presets"). full-width and
* report render one column (report centers a constrained-width column);
/** Desktop column count for each flow preset (#280 "Presets"; full-width
* removed #321). report renders one column (centered, constrained-width);
* columns-2/columns-3 render two/three equal columns. */
export const FLOW_PRESET_COLUMNS: Record<FlowPresetV1, number> = {
'full-width': 1, report: 1, 'columns-2': 2, 'columns-3': 3,
report: 1, 'columns-2': 2, 'columns-3': 3,
};

const FLOW_PRESETS = new Set<string>(Object.keys(FLOW_PRESET_COLUMNS));
Expand All @@ -141,7 +141,8 @@ const FLOW_PRESETS = new Set<string>(Object.keys(FLOW_PRESET_COLUMNS));
export const FLOW_MOBILE_BREAKPOINT = 768;

/** The desktop column count for a preset; an unknown/absent preset falls back
* to full-width (1). */
* to 1 column (the same column count as `report`, the nearest valid single-
* column preset since full-width was removed, #321). */
export function presetColumns(preset: unknown): number {
return typeof preset === 'string' && Object.hasOwn(FLOW_PRESET_COLUMNS, preset)
? FLOW_PRESET_COLUMNS[preset as FlowPresetV1] : 1;
Expand Down Expand Up @@ -238,7 +239,7 @@ export function computeFlowLayout(input: ComputeFlowLayoutInput): FlowLayoutMode
const { tiles, layout, mobile = false } = input;
const surface = flowSurface(layout);
const rawPreset = surface && typeof surface.preset === 'string' ? surface.preset : undefined;
const preset: FlowPresetV1 = rawPreset && FLOW_PRESETS.has(rawPreset) ? rawPreset as FlowPresetV1 : 'full-width';
const preset: FlowPresetV1 = rawPreset && FLOW_PRESETS.has(rawPreset) ? rawPreset as FlowPresetV1 : 'report';
const items = surface && isObject(surface.items) ? surface.items as Record<string, unknown> : {};
const columns = mobile ? 1 : presetColumns(preset);

Expand Down
38 changes: 32 additions & 6 deletions src/dashboard/layouts/grafana-grid-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,14 @@ export interface GrafanaGridTileRender {
tileId: string;
index: number;
span: number;
/** The tile's resolved STORED span (`resolveGridPlacement(items[id]).span`)
* before any render-mode override and before the effective-columns clamp
* (#321 Full view). In `'tiles'` mode this equals the clamped `span`'s
* pre-clamp source value; in `'full'` mode `span` is overwritten to
* `columns` for the row-per-tile layout, so `persistedSpan` is the only
* place the unchanged stored span still travels — the UI's full-view
* resize must persist THIS value, never the overridden `span`. */
persistedSpan: number;
/** Row units (1..16), already canonicalized/defaulted by
* `resolveGridPlacement` — never the legacy string form (#291
* height-units follow-up: renamed from `height` so a discriminating
Expand Down Expand Up @@ -329,6 +337,13 @@ export interface GrafanaGridVisibleTile {
isKpi?: boolean;
}

/** The grafana-grid@1 render mode (#321 "Full view"): `'tiles'` is today's
* packed multi-tile-per-row grid; `'full'` renders every visible tile at the
* full effective column count — one tile per row — for the transient
* "Full view" render mode. Persistence is unaffected either way: the stored
* placement (`persistedSpan`) is never rewritten by a render-mode change. */
export type GridRenderMode = 'tiles' | 'full';

export interface ComputeGrafanaGridLayoutInput {
tiles: readonly GrafanaGridVisibleTile[];
/** The grafana-grid layout document (or any object whose `items` holds
Expand All @@ -337,6 +352,9 @@ export interface ComputeGrafanaGridLayoutInput {
layout: unknown;
/** The rendering container's width in px; see `effectiveGridColumns`. */
containerWidth?: number;
/** Render mode (#321); defaults to `'tiles'` (today's packed behavior)
* when absent. */
renderMode?: GridRenderMode;
}

function gridItemsFor(layout: unknown): Record<string, unknown> {
Expand All @@ -351,21 +369,28 @@ function gridItemsFor(layout: unknown): Record<string, unknown> {
* no row-grouping type, band, or fold (rowless). Pure and non-mutating.
*/
export function computeGrafanaGridLayout(input: ComputeGrafanaGridLayoutInput): GrafanaGridLayoutModel {
const { tiles, layout, containerWidth } = input;
const { tiles, layout, containerWidth, renderMode = 'tiles' } = input;
const columns = effectiveGridColumns(containerWidth);
const items = gridItemsFor(layout);

let row = 0;
let cursor = 0;
const renders: GrafanaGridTileRender[] = tiles.map((tile, index) => {
const placement = resolveGridPlacement(items[tile.id]);
const span = effectiveGridSpan(placement.span, columns);
const span = renderMode === 'full' ? columns : effectiveGridSpan(placement.span, columns);
if (cursor + span > columns) {
row += 1;
cursor = 0;
}
const render: GrafanaGridTileRender = {
tileId: tile.id, index, span, heightUnits: placement.height, isKpi: !!tile.isKpi, row, colStart: cursor,
tileId: tile.id,
index,
span,
persistedSpan: placement.span,
heightUnits: placement.height,
isKpi: !!tile.isKpi,
row,
colStart: cursor,
};
cursor += span;
return render;
Expand All @@ -389,8 +414,9 @@ export interface GrafanaGridFallbackTile {
* an explicit flow item — even one with no persisted grid placement, which
* resolves to the grid default (span 6) and maps to its flow equivalent
* (span 2), rather than silently falling through to flow's own unrelated
* default (span 1). `full-width` is the fallback preset: the closest single-
* column analog to a rowless grid with no fixed column count.
* default (span 1). `columns-2` is the fallback preset (#321: full-width was
* removed from flow@1 entirely) — the canonical remaining single-decision
* fallback for a rowless grid with no fixed column count.
*/
export function deriveFlowFallback(
gridLayout: unknown, tiles: readonly GrafanaGridFallbackTile[],
Expand All @@ -403,7 +429,7 @@ export function deriveFlowFallback(
span: flowSpanFromGridSpan(gridPlacement.span), height: gridHeightUnitsToFlowHeight(gridPlacement.height),
};
}
return { type: 'flow', version: 1, preset: 'full-width', items: flowItems };
return { type: 'flow', version: 1, preset: 'columns-2', items: flowItems };
}

// ── Pure resize math (#291 Wave 3 — corner-drag resize): the DOM listener in
Expand Down
6 changes: 3 additions & 3 deletions src/generated/json-schema-validators.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/generated/json-schema.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -628,9 +628,9 @@ export interface LibraryV2 {
/**
* Flow preset
*
* Desktop column arrangement: full-width and report render one column (report centers a constrained-width column), columns-2 and columns-3 render equal columns.
* Desktop column arrangement: report renders one constrained-width centered column, columns-2 and columns-3 render equal columns.
*/
export type FlowPresetV1 = "full-width" | "report" | "columns-2" | "columns-3";
export type FlowPresetV1 = "report" | "columns-2" | "columns-3";

/**
* Tile height
Expand Down
3 changes: 1 addition & 2 deletions src/generated/json-schemas.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading