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
This commit is contained in:
Oscar Kilhed
2025-02-27 15:11:54 +01:00
committed by GitHub
parent 21be5e3f45
commit f54910342d
2 changed files with 213 additions and 4 deletions
@@ -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: [] } },
},
},
],
},
});
});
});
@@ -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),
});
});