Skip to content

Commit

Permalink
Dynamic dashboards: Add row repeat serialization/deserialization (gra…
Browse files Browse the repository at this point in the history
…fana#100826)

* add row repeat serialization/deserialization

* prettier

* Add tests for deserializing repeated rows. Also add tests for serilizing that was missing
  • Loading branch information
oscarkilhed authored Feb 27, 2025
1 parent 21be5e3 commit f549103
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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: [],
},
},
},
},
],
Expand All @@ -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: [],
},
},
},
},
{
Expand Down Expand Up @@ -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: [] } },
},
},
],
},
});
});
});
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -17,14 +19,26 @@ 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,
collapsed: row.state.isCollapsed ?? false,
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;
}),
},
};
Expand All @@ -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),
});
});
Expand Down

0 comments on commit f549103

Please sign in to comment.