From f54910342d81bdab082c5148941bd544ca6a4060 Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Thu, 27 Feb 2025 15:11:54 +0100 Subject: [PATCH] Dynamic dashboards: Add row repeat serialization/deserialization (#100826) * add row repeat serialization/deserialization * prettier * Add tests for deserializing repeated rows. Also add tests for serilizing that was missing --- .../RowsLayoutSerializer.test.ts | 194 +++++++++++++++++- .../layoutSerializers/RowsLayoutSerializer.ts | 23 ++- 2 files changed, 213 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.test.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.test.ts index ffd9241e0f5..cd483cafa0c 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.test.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.test.ts @@ -1,7 +1,10 @@ +import { SceneCSSGridLayout, SceneGridLayout } from '@grafana/scenes'; import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; import { ResponsiveGridLayoutManager } from '../../scene/layout-responsive-grid/ResponsiveGridLayoutManager'; +import { RowItem } from '../../scene/layout-rows/RowItem'; +import { RowItemRepeaterBehavior } from '../../scene/layout-rows/RowItemRepeaterBehavior'; import { RowsLayoutManager } from '../../scene/layout-rows/RowsLayoutManager'; import { RowsLayoutSerializer } from './RowsLayoutSerializer'; @@ -39,7 +42,14 @@ describe('deserialization', () => { spec: { title: 'Row 1', collapsed: false, - layout: { kind: 'ResponsiveGridLayout', spec: { row: '', col: '', items: [] } }, + layout: { + kind: 'ResponsiveGridLayout', + spec: { + row: 'minmax(min-content, max-content)', + col: 'repeat(auto-fit, minmax(400px, 1fr))', + items: [], + }, + }, }, }, ], @@ -61,7 +71,14 @@ describe('deserialization', () => { spec: { title: 'Row 1', collapsed: false, - layout: { kind: 'ResponsiveGridLayout', spec: { row: '', col: '', items: [] } }, + layout: { + kind: 'ResponsiveGridLayout', + spec: { + row: 'minmax(min-content, max-content)', + col: 'repeat(auto-fit, minmax(400px, 1fr))', + items: [], + }, + }, }, }, { @@ -97,4 +114,177 @@ describe('deserialization', () => { expect(deserialized).toBeInstanceOf(RowsLayoutManager); expect(deserialized.state.rows).toHaveLength(0); }); + + it('should deserialize row with repeat behavior', () => { + const layout: DashboardV2Spec['layout'] = { + kind: 'RowsLayout', + spec: { + rows: [ + { + kind: 'RowsLayoutRow', + spec: { + title: 'Repeated Row', + collapsed: false, + layout: { kind: 'GridLayout', spec: { items: [] } }, + repeat: { value: 'foo', mode: 'variable' }, + }, + }, + ], + }, + }; + const serializer = new RowsLayoutSerializer(); + const deserialized = serializer.deserialize(layout, {}, false); + + expect(deserialized).toBeInstanceOf(RowsLayoutManager); + expect(deserialized.state.rows).toHaveLength(1); + + const row = deserialized.state.rows[0]; + expect(row.state.$behaviors).toBeDefined(); + const behaviors = row.state.$behaviors ?? []; + expect(behaviors).toHaveLength(1); + const repeaterBehavior = behaviors[0] as RowItemRepeaterBehavior; + expect(repeaterBehavior).toBeInstanceOf(RowItemRepeaterBehavior); + expect(repeaterBehavior.state.variableName).toBe('foo'); + }); +}); + +describe('serialization', () => { + it('should serialize basic row layout', () => { + const rowsLayout = new RowsLayoutManager({ + rows: [ + new RowItem({ + title: 'Row 1', + isCollapsed: false, + layout: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [], + isDraggable: true, + isResizable: true, + }), + }), + }), + ], + }); + + const serializer = new RowsLayoutSerializer(); + const serialized = serializer.serialize(rowsLayout); + + expect(serialized).toEqual({ + kind: 'RowsLayout', + spec: { + rows: [ + { + kind: 'RowsLayoutRow', + spec: { + title: 'Row 1', + collapsed: false, + layout: { kind: 'GridLayout', spec: { items: [] } }, + }, + }, + ], + }, + }); + }); + + it('should serialize row with repeat behavior', () => { + const rowsLayout = new RowsLayoutManager({ + rows: [ + new RowItem({ + title: 'Repeated Row', + isCollapsed: false, + layout: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [], + isDraggable: true, + isResizable: true, + }), + }), + $behaviors: [new RowItemRepeaterBehavior({ variableName: 'foo' })], + }), + ], + }); + + const serializer = new RowsLayoutSerializer(); + const serialized = serializer.serialize(rowsLayout); + + expect(serialized).toEqual({ + kind: 'RowsLayout', + spec: { + rows: [ + { + kind: 'RowsLayoutRow', + spec: { + title: 'Repeated Row', + collapsed: false, + layout: { kind: 'GridLayout', spec: { items: [] } }, + repeat: { value: 'foo', mode: 'variable' }, + }, + }, + ], + }, + }); + }); + + it('should serialize multiple rows with different layouts', () => { + const rowsLayout = new RowsLayoutManager({ + rows: [ + new RowItem({ + title: 'Row 1', + isCollapsed: false, + layout: new ResponsiveGridLayoutManager({ + layout: new SceneCSSGridLayout({ + children: [], + templateColumns: 'repeat(auto-fit, minmax(400px, 1fr))', + autoRows: 'minmax(min-content, max-content)', + }), + }), + }), + new RowItem({ + title: 'Row 2', + isCollapsed: true, + layout: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [], + isDraggable: true, + isResizable: true, + }), + }), + }), + ], + }); + + const serializer = new RowsLayoutSerializer(); + const serialized = serializer.serialize(rowsLayout); + + expect(serialized).toEqual({ + kind: 'RowsLayout', + spec: { + rows: [ + { + kind: 'RowsLayoutRow', + spec: { + title: 'Row 1', + collapsed: false, + layout: { + kind: 'ResponsiveGridLayout', + spec: { + row: 'minmax(min-content, max-content)', + col: 'repeat(auto-fit, minmax(400px, 1fr))', + items: [], + }, + }, + }, + }, + { + kind: 'RowsLayoutRow', + spec: { + title: 'Row 2', + collapsed: true, + layout: { kind: 'GridLayout', spec: { items: [] } }, + }, + }, + ], + }, + }); + }); }); diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts index d1823d6b1a7..c2b9980b55c 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts @@ -1,6 +1,8 @@ -import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; +import { SceneObject } from '@grafana/scenes'; +import { DashboardV2Spec, RowsLayoutRowKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; import { RowItem } from '../../scene/layout-rows/RowItem'; +import { RowItemRepeaterBehavior } from '../../scene/layout-rows/RowItemRepeaterBehavior'; import { RowsLayoutManager } from '../../scene/layout-rows/RowsLayoutManager'; import { LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager'; @@ -17,7 +19,7 @@ export class RowsLayoutSerializer implements LayoutManagerSerializer { if (layout.kind === 'RowsLayout') { throw new Error('Nested RowsLayout is not supported'); } - return { + const rowKind: RowsLayoutRowKind = { kind: 'RowsLayoutRow', spec: { title: row.state.title, @@ -25,6 +27,18 @@ export class RowsLayoutSerializer implements LayoutManagerSerializer { layout: layout, }, }; + + if (row.state.$behaviors) { + for (const behavior of row.state.$behaviors) { + if (behavior instanceof RowItemRepeaterBehavior) { + if (rowKind.spec.repeat) { + throw new Error('Multiple repeaters are not supported'); + } + rowKind.spec.repeat = { value: behavior.state.variableName, mode: 'variable' }; + } + } + } + return rowKind; }), }, }; @@ -40,9 +54,14 @@ export class RowsLayoutSerializer implements LayoutManagerSerializer { } const rows = layout.spec.rows.map((row) => { const layout = row.spec.layout; + const behaviors: SceneObject[] = []; + if (row.spec.repeat) { + behaviors.push(new RowItemRepeaterBehavior({ variableName: row.spec.repeat.value })); + } return new RowItem({ title: row.spec.title, isCollapsed: row.spec.collapsed, + $behaviors: behaviors, layout: layoutSerializerRegistry.get(layout.kind).serializer.deserialize(layout, elements, preload), }); });