Dynamic dashboards: Refactor serializing and deserializing layout managers to simplify (#103008)

Refactor serializing and deserializing layout managers to simplify
This commit is contained in:
Oscar Kilhed
2025-03-27 17:25:53 +01:00
committed by GitHub
parent 69803300f1
commit 38c96c4c7d
17 changed files with 288 additions and 284 deletions
@@ -12,6 +12,7 @@ import {
useSceneObjectState,
SceneGridLayoutDragStartEvent,
} from '@grafana/scenes';
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { GRID_COLUMN_COUNT } from 'app/core/constants';
import { t } from 'app/core/internationalization';
import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty';
@@ -21,6 +22,7 @@ import {
ObjectRemovedFromCanvasEvent,
ObjectsReorderedOnCanvasEvent,
} from '../../edit-pane/shared';
import { serializeDefaultGridLayout } from '../../serialization/layoutSerializers/DefaultGridLayoutSerializer';
import { isClonedKey, joinCloneKeys } from '../../utils/clone';
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
import {
@@ -61,10 +63,13 @@ export class DefaultGridLayoutManager
},
id: 'default-grid',
createFromLayout: DefaultGridLayoutManager.createFromLayout,
kind: 'GridLayout',
isGridLayout: true,
};
public serialize(): DashboardV2Spec['layout'] {
return serializeDefaultGridLayout(this);
}
public readonly descriptor = DefaultGridLayoutManager.descriptor;
public constructor(state: DefaultGridLayoutManagerState) {
@@ -1,9 +1,11 @@
import { SceneComponentProps, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes';
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { GRID_CELL_VMARGIN } from 'app/core/constants';
import { t } from 'app/core/internationalization';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
import { NewObjectAddedToCanvasEvent, ObjectRemovedFromCanvasEvent } from '../../edit-pane/shared';
import { serializeAutoGridLayout } from '../../serialization/layoutSerializers/ResponsiveGridLayoutSerializer';
import { joinCloneKeys } from '../../utils/clone';
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
import {
@@ -52,10 +54,13 @@ export class AutoGridLayoutManager
},
id: 'auto-grid',
createFromLayout: AutoGridLayoutManager.createFromLayout,
kind: 'AutoGridLayout',
isGridLayout: true,
};
public serialize(): DashboardV2Spec['layout'] {
return serializeAutoGridLayout(this);
}
public readonly descriptor = AutoGridLayoutManager.descriptor;
public constructor(state: Partial<AutoGridLayoutManagerState>) {
@@ -1,4 +1,5 @@
import { SceneGridItemLike, SceneGridRow, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes';
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { t } from 'app/core/internationalization';
import {
@@ -6,6 +7,7 @@ import {
ObjectRemovedFromCanvasEvent,
ObjectsReorderedOnCanvasEvent,
} from '../../edit-pane/shared';
import { serializeRowsLayout } from '../../serialization/layoutSerializers/RowsLayoutSerializer';
import { isClonedKey } from '../../utils/clone';
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
import { DashboardGridItem } from '../layout-default/DashboardGridItem';
@@ -37,10 +39,13 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
},
id: 'rows-layout',
createFromLayout: RowsLayoutManager.createFromLayout,
kind: 'RowsLayout',
isGridLayout: false,
};
public serialize(): DashboardV2Spec['layout'] {
return serializeRowsLayout(this);
}
public readonly descriptor = RowsLayoutManager.descriptor;
public addPanel(vizPanel: VizPanel) {
@@ -5,6 +5,7 @@ import {
SceneObjectUrlValues,
VizPanel,
} from '@grafana/scenes';
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { t } from 'app/core/internationalization';
import {
@@ -12,6 +13,7 @@ import {
ObjectRemovedFromCanvasEvent,
ObjectsReorderedOnCanvasEvent,
} from '../../edit-pane/shared';
import { serializeTabsLayout } from '../../serialization/layoutSerializers/TabsLayoutSerializer';
import { RowItem } from '../layout-rows/RowItem';
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
@@ -39,10 +41,13 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
},
id: 'tabs-layout',
createFromLayout: TabsLayoutManager.createFromLayout,
kind: 'TabsLayout',
isGridLayout: false,
};
public serialize(): DashboardV2Spec['layout'] {
return serializeTabsLayout(this);
}
public readonly descriptor = TabsLayoutManager.descriptor;
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: () => [this.getUrlKey()] });
@@ -17,6 +17,11 @@ export interface DashboardLayoutManager<S = {}> extends SceneObject {
*/
descriptor: Readonly<LayoutRegistryItem>;
/**
* Serializer for layout
*/
serialize(): DashboardV2Spec['layout'];
/**
* Adds a new panel to the layout
*/
@@ -1,29 +1,17 @@
import { RegistryItem } from '@grafana/data';
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { DashboardLayoutManager } from './DashboardLayoutManager';
/**
* The layout descriptor used when selecting / switching layouts
*/
export interface LayoutRegistryItem<S = {}> extends RegistryItem {
export interface LayoutRegistryItem extends RegistryItem {
/**
* When switching between layouts
* @param currentLayout
*/
createFromLayout(currentLayout: DashboardLayoutManager): DashboardLayoutManager;
/**
* Create from persisted state
* @param saveModel
*/
createFromSaveModel?(saveModel: S): void;
/**
* Schema kind of layout
*/
kind?: DashboardV2Spec['layout']['kind'];
/**
* Is grid layout (that contains panels)
*/
@@ -16,7 +16,6 @@ import { DashboardGridItem } from '../../scene/layout-default/DashboardGridItem'
import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager';
import { RowRepeaterBehavior } from '../../scene/layout-default/RowRepeaterBehavior';
import { RowActions } from '../../scene/layout-default/row-actions/RowActions';
import { DashboardLayoutManager, LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager';
import { getOriginalKey, isClonedKey } from '../../utils/clone';
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
import { calculateGridItemDimensions, isLibraryPanel } from '../../utils/utils';
@@ -24,31 +23,32 @@ import { GRID_ROW_HEIGHT } from '../const';
import { buildLibraryPanel, buildVizPanel } from './utils';
export class DefaultGridLayoutManagerSerializer implements LayoutManagerSerializer {
serialize(layoutManager: DefaultGridLayoutManager, isSnapshot?: boolean): DashboardV2Spec['layout'] {
return {
kind: 'GridLayout',
spec: {
items: getGridLayoutItems(layoutManager, isSnapshot),
},
};
}
export function serializeDefaultGridLayout(
layoutManager: DefaultGridLayoutManager,
isSnapshot?: boolean
): DashboardV2Spec['layout'] {
return {
kind: 'GridLayout',
spec: {
items: getGridLayoutItems(layoutManager, isSnapshot),
},
};
}
deserialize(
layout: DashboardV2Spec['layout'],
elements: DashboardV2Spec['elements'],
preload: boolean
): DashboardLayoutManager {
if (layout.kind !== 'GridLayout') {
throw new Error('Invalid layout kind');
}
return new DefaultGridLayoutManager({
grid: new SceneGridLayout({
isLazy: !(preload || contextSrv.user.authenticatedBy === 'render'),
children: createSceneGridLayoutForItems(layout, elements),
}),
});
export function deserializeDefaultGridLayout(
layout: DashboardV2Spec['layout'],
elements: DashboardV2Spec['elements'],
preload: boolean
): DefaultGridLayoutManager {
if (layout.kind !== 'GridLayout') {
throw new Error('Invalid layout kind');
}
return new DefaultGridLayoutManager({
grid: new SceneGridLayout({
isLazy: !(preload || contextSrv.user.authenticatedBy === 'render'),
children: createSceneGridLayoutForItems(layout, elements),
}),
});
}
function getGridLayoutItems(
@@ -15,99 +15,99 @@ import {
getTemplateColumnsTemplate,
AutoGridLayoutManager,
} from '../../scene/layout-responsive-grid/ResponsiveGridLayoutManager';
import { DashboardLayoutManager, LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager';
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
import { getGridItemKeyForPanelId } from '../../utils/utils';
import { buildLibraryPanel, buildVizPanel, getConditionalRendering } from './utils';
export class AutoGridLayoutSerializer implements LayoutManagerSerializer {
serialize(layoutManager: AutoGridLayoutManager): DashboardV2Spec['layout'] {
const { maxColumnCount, fillScreen, columnWidth, rowHeight, layout } = layoutManager.state;
const defaults = defaultAutoGridLayoutSpec();
export function serializeAutoGridLayout(layoutManager: AutoGridLayoutManager): DashboardV2Spec['layout'] {
const { maxColumnCount, fillScreen, columnWidth, rowHeight, layout } = layoutManager.state;
const defaults = defaultAutoGridLayoutSpec();
return {
kind: 'AutoGridLayout',
spec: {
maxColumnCount,
fillScreen: fillScreen === defaults.fillScreen ? undefined : fillScreen,
...serializeAutoGridColumnWidth(columnWidth),
...serializeAutoGridRowHeight(rowHeight),
items: layout.state.children.map((child) => {
if (!(child instanceof AutoGridItem)) {
throw new Error('Expected AutoGridItem');
}
// For serialization we should retrieve the original element key
const elementKey = dashboardSceneGraph.getElementIdentifierForVizPanel(child.state?.body);
const layoutItem: AutoGridLayoutItemKind = {
kind: 'AutoGridLayoutItem',
spec: {
element: {
kind: 'ElementReference',
name: elementKey,
},
},
};
const conditionalRenderingRootGroup = child.state.conditionalRendering?.serialize();
// Only serialize the conditional rendering if it has items
if (conditionalRenderingRootGroup?.spec.items.length) {
layoutItem.spec.conditionalRendering = conditionalRenderingRootGroup;
}
if (child.state.variableName) {
layoutItem.spec.repeat = {
mode: 'variable',
value: child.state.variableName,
};
}
return layoutItem;
}),
},
};
}
deserialize(layout: DashboardV2Spec['layout'], elements: DashboardV2Spec['elements']): DashboardLayoutManager {
if (layout.kind !== 'AutoGridLayout') {
throw new Error('Invalid layout kind');
}
const defaults = defaultAutoGridLayoutSpec();
const { maxColumnCount, columnWidthMode, columnWidth, rowHeightMode, rowHeight, fillScreen } = layout.spec;
const children = layout.spec.items.map((item) => {
const panel = elements[item.spec.element.name];
if (!panel) {
throw new Error(`Panel with uid ${item.spec.element.name} not found in the dashboard elements`);
}
return new AutoGridItem({
key: getGridItemKeyForPanelId(panel.spec.id),
body: panel.kind === 'LibraryPanel' ? buildLibraryPanel(panel) : buildVizPanel(panel),
variableName: item.spec.repeat?.value,
conditionalRendering: getConditionalRendering(item),
});
});
const columnWidthCombined = columnWidthMode === 'custom' ? columnWidth : columnWidthMode;
const rowHeightCombined = rowHeightMode === 'custom' ? rowHeight : rowHeightMode;
return new AutoGridLayoutManager({
return {
kind: 'AutoGridLayout',
spec: {
maxColumnCount,
columnWidth: columnWidthCombined,
rowHeight: rowHeightCombined,
fillScreen: fillScreen ?? defaults.fillScreen,
layout: new AutoGridLayout({
templateColumns: getTemplateColumnsTemplate(
maxColumnCount ?? defaults.maxColumnCount!,
columnWidthCombined ?? AUTO_GRID_DEFAULT_COLUMN_WIDTH
),
autoRows: getAutoRowsTemplate(rowHeightCombined ?? AUTO_GRID_DEFAULT_ROW_HEIGHT, fillScreen ?? false),
children,
fillScreen: fillScreen === defaults.fillScreen ? undefined : fillScreen,
...serializeAutoGridColumnWidth(columnWidth),
...serializeAutoGridRowHeight(rowHeight),
items: layout.state.children.map((child) => {
if (!(child instanceof AutoGridItem)) {
throw new Error('Expected AutoGridItem');
}
// For serialization we should retrieve the original element key
const elementKey = dashboardSceneGraph.getElementIdentifierForVizPanel(child.state?.body);
const layoutItem: AutoGridLayoutItemKind = {
kind: 'AutoGridLayoutItem',
spec: {
element: {
kind: 'ElementReference',
name: elementKey,
},
},
};
const conditionalRenderingRootGroup = child.state.conditionalRendering?.serialize();
// Only serialize the conditional rendering if it has items
if (conditionalRenderingRootGroup?.spec.items.length) {
layoutItem.spec.conditionalRendering = conditionalRenderingRootGroup;
}
if (child.state.variableName) {
layoutItem.spec.repeat = {
mode: 'variable',
value: child.state.variableName,
};
}
return layoutItem;
}),
});
},
};
}
export function deserializeAutoGridLayout(
layout: DashboardV2Spec['layout'],
elements: DashboardV2Spec['elements']
): AutoGridLayoutManager {
if (layout.kind !== 'AutoGridLayout') {
throw new Error('Invalid layout kind');
}
const defaults = defaultAutoGridLayoutSpec();
const { maxColumnCount, columnWidthMode, columnWidth, rowHeightMode, rowHeight, fillScreen } = layout.spec;
const children = layout.spec.items.map((item) => {
const panel = elements[item.spec.element.name];
if (!panel) {
throw new Error(`Panel with uid ${item.spec.element.name} not found in the dashboard elements`);
}
return new AutoGridItem({
key: getGridItemKeyForPanelId(panel.spec.id),
body: panel.kind === 'LibraryPanel' ? buildLibraryPanel(panel) : buildVizPanel(panel),
variableName: item.spec.repeat?.value,
conditionalRendering: getConditionalRendering(item),
});
});
const columnWidthCombined = columnWidthMode === 'custom' ? columnWidth : columnWidthMode;
const rowHeightCombined = rowHeightMode === 'custom' ? rowHeight : rowHeightMode;
return new AutoGridLayoutManager({
maxColumnCount,
columnWidth: columnWidthCombined,
rowHeight: rowHeightCombined,
fillScreen: fillScreen ?? defaults.fillScreen,
layout: new AutoGridLayout({
templateColumns: getTemplateColumnsTemplate(
maxColumnCount ?? defaults.maxColumnCount!,
columnWidthCombined ?? AUTO_GRID_DEFAULT_COLUMN_WIDTH
),
autoRows: getAutoRowsTemplate(rowHeightCombined ?? AUTO_GRID_DEFAULT_ROW_HEIGHT, fillScreen ?? false),
children,
}),
});
}
function serializeAutoGridColumnWidth(columnWidth: AutoGridColumnWidth) {
@@ -8,7 +8,7 @@ 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';
import { deserializeRowsLayout, serializeRowsLayout } from './RowsLayoutSerializer';
describe('deserialization', () => {
it('should deserialize rows layout with default grid child', () => {
@@ -27,8 +27,7 @@ describe('deserialization', () => {
],
},
};
const serializer = new RowsLayoutSerializer();
const deserialized = serializer.deserialize(layout, {}, false);
const deserialized = deserializeRowsLayout(layout, {}, false);
expect(deserialized).toBeInstanceOf(RowsLayoutManager);
expect(deserialized.state.rows[0].state.layout).toBeInstanceOf(DefaultGridLayoutManager);
});
@@ -51,8 +50,7 @@ describe('deserialization', () => {
],
},
};
const serializer = new RowsLayoutSerializer();
const deserialized = serializer.deserialize(layout, {}, false);
const deserialized = deserializeRowsLayout(layout, {}, false);
expect(deserialized).toBeInstanceOf(RowsLayoutManager);
expect(deserialized.state.rows[0].state.layout).toBeInstanceOf(DefaultGridLayoutManager);
expect(deserialized.state.rows[0].state.collapse).toBe(true);
@@ -84,8 +82,7 @@ describe('deserialization', () => {
],
},
};
const serializer = new RowsLayoutSerializer();
const deserialized = serializer.deserialize(layout, {}, false);
const deserialized = deserializeRowsLayout(layout, {}, false);
expect(deserialized).toBeInstanceOf(RowsLayoutManager);
expect(deserialized.state.rows[0].state.layout).toBeInstanceOf(AutoGridLayoutManager);
});
@@ -126,8 +123,7 @@ describe('deserialization', () => {
],
},
};
const serializer = new RowsLayoutSerializer();
const deserialized = serializer.deserialize(layout, {}, false);
const deserialized = deserializeRowsLayout(layout, {}, false);
expect(deserialized).toBeInstanceOf(RowsLayoutManager);
expect(deserialized.state.rows).toHaveLength(2);
expect(deserialized.state.rows[0].state.layout).toBeInstanceOf(AutoGridLayoutManager);
@@ -143,8 +139,7 @@ describe('deserialization', () => {
rows: [],
},
};
const serializer = new RowsLayoutSerializer();
const deserialized = serializer.deserialize(layout, {}, false);
const deserialized = deserializeRowsLayout(layout, {}, false);
expect(deserialized).toBeInstanceOf(RowsLayoutManager);
expect(deserialized.state.rows).toHaveLength(0);
});
@@ -168,8 +163,7 @@ describe('deserialization', () => {
],
},
};
const serializer = new RowsLayoutSerializer();
const deserialized = serializer.deserialize(layout, {}, false);
const deserialized = deserializeRowsLayout(layout, {}, false);
expect(deserialized).toBeInstanceOf(RowsLayoutManager);
expect(deserialized.state.rows).toHaveLength(1);
@@ -202,8 +196,7 @@ describe('serialization', () => {
],
});
const serializer = new RowsLayoutSerializer();
const serialized = serializer.serialize(rowsLayout);
const serialized = serializeRowsLayout(rowsLayout);
expect(serialized).toEqual({
kind: 'RowsLayout',
@@ -243,8 +236,7 @@ describe('serialization', () => {
],
});
const serializer = new RowsLayoutSerializer();
const serialized = serializer.serialize(rowsLayout);
const serialized = serializeRowsLayout(rowsLayout);
expect(serialized).toEqual({
kind: 'RowsLayout',
@@ -283,8 +275,7 @@ describe('serialization', () => {
],
});
const serializer = new RowsLayoutSerializer();
const serialized = serializer.serialize(rowsLayout);
const serialized = serializeRowsLayout(rowsLayout);
expect(serialized).toEqual({
kind: 'RowsLayout',
@@ -335,8 +326,7 @@ describe('serialization', () => {
],
});
const serializer = new RowsLayoutSerializer();
const serialized = serializer.serialize(rowsLayout);
const serialized = serializeRowsLayout(rowsLayout);
expect(serialized).toEqual({
kind: 'RowsLayout',
@@ -4,76 +4,73 @@ import { DashboardV2Spec, RowsLayoutRowKind } from '@grafana/schema/dist/esm/sch
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';
import { layoutSerializerRegistry } from './layoutSerializerRegistry';
import { getConditionalRendering, getLayout } from './utils';
import { layoutDeserializerRegistry } from './layoutSerializerRegistry';
import { getConditionalRendering } from './utils';
export class RowsLayoutSerializer implements LayoutManagerSerializer {
serialize(layoutManager: RowsLayoutManager): DashboardV2Spec['layout'] {
return {
kind: 'RowsLayout',
spec: {
rows: layoutManager.state.rows.map((row) => {
const layout = getLayout(row.state.layout);
const rowKind: RowsLayoutRowKind = {
kind: 'RowsLayoutRow',
spec: {
title: row.state.title,
collapse: row.state.collapse,
layout: layout,
fillScreen: row.state.fillScreen,
hideHeader: row.state.hideHeader,
},
};
export function serializeRowsLayout(layoutManager: RowsLayoutManager): DashboardV2Spec['layout'] {
return {
kind: 'RowsLayout',
spec: {
rows: layoutManager.state.rows.map((row) => {
const layout = row.state.layout.serialize();
const rowKind: RowsLayoutRowKind = {
kind: 'RowsLayoutRow',
spec: {
title: row.state.title,
collapse: row.state.collapse,
layout: layout,
fillScreen: row.state.fillScreen,
hideHeader: row.state.hideHeader,
},
};
const conditionalRenderingRootGroup = row.state.conditionalRendering?.serialize();
// Only serialize the conditional rendering if it has items
if (conditionalRenderingRootGroup?.spec.items.length) {
rowKind.spec.conditionalRendering = conditionalRenderingRootGroup;
}
const conditionalRenderingRootGroup = row.state.conditionalRendering?.serialize();
// Only serialize the conditional rendering if it has items
if (conditionalRenderingRootGroup?.spec.items.length) {
rowKind.spec.conditionalRendering = conditionalRenderingRootGroup;
}
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' };
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;
}),
},
};
}
return rowKind;
}),
},
};
}
export function deserializeRowsLayout(
layout: DashboardV2Spec['layout'],
elements: DashboardV2Spec['elements'],
preload: boolean
): RowsLayoutManager {
if (layout.kind !== 'RowsLayout') {
throw new Error('Invalid layout kind');
}
deserialize(
layout: DashboardV2Spec['layout'],
elements: DashboardV2Spec['elements'],
preload: boolean
): RowsLayoutManager {
if (layout.kind !== 'RowsLayout') {
throw new Error('Invalid layout kind');
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 }));
}
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,
collapse: row.spec.collapse,
hideHeader: row.spec.hideHeader,
fillScreen: row.spec.fillScreen,
$behaviors: behaviors,
layout: layoutSerializerRegistry.get(layout.kind).serializer.deserialize(layout, elements, preload),
conditionalRendering: getConditionalRendering(row),
});
return new RowItem({
title: row.spec.title,
collapse: row.spec.collapse,
hideHeader: row.spec.hideHeader,
fillScreen: row.spec.fillScreen,
$behaviors: behaviors,
layout: layoutDeserializerRegistry.get(layout.kind).deserialize(layout, elements, preload),
conditionalRendering: getConditionalRendering(row),
});
return new RowsLayoutManager({ rows });
}
});
return new RowsLayoutManager({ rows });
}
@@ -5,7 +5,7 @@ import { AutoGridLayoutManager } from '../../scene/layout-responsive-grid/Respon
import { RowsLayoutManager } from '../../scene/layout-rows/RowsLayoutManager';
import { TabsLayoutManager } from '../../scene/layout-tabs/TabsLayoutManager';
import { TabsLayoutSerializer } from './TabsLayoutSerializer';
import { deserializeTabsLayout } from './TabsLayoutSerializer';
describe('deserialization', () => {
it('should deserialize tabs layout with row child', () => {
@@ -15,8 +15,7 @@ describe('deserialization', () => {
tabs: [{ kind: 'TabsLayoutTab', spec: { title: 'Tab 1', layout: { kind: 'RowsLayout', spec: { rows: [] } } } }],
},
};
const serializer = new TabsLayoutSerializer();
const deserialized = serializer.deserialize(layout, {}, false);
const deserialized = deserializeTabsLayout(layout, {}, false);
expect(deserialized).toBeInstanceOf(TabsLayoutManager);
expect(deserialized.state.tabs[0].state.layout).toBeInstanceOf(RowsLayoutManager);
});
@@ -39,8 +38,7 @@ describe('deserialization', () => {
],
},
};
const serializer = new TabsLayoutSerializer();
const deserialized = serializer.deserialize(layout, {}, false);
const deserialized = deserializeTabsLayout(layout, {}, false);
expect(deserialized).toBeInstanceOf(TabsLayoutManager);
expect(deserialized.state.tabs[0].state.layout).toBeInstanceOf(AutoGridLayoutManager);
});
@@ -57,8 +55,7 @@ describe('deserialization', () => {
],
},
};
const serializer = new TabsLayoutSerializer();
const deserialized = serializer.deserialize(layout, {}, false);
const deserialized = deserializeTabsLayout(layout, {}, false);
expect(deserialized).toBeInstanceOf(TabsLayoutManager);
expect(deserialized.state.tabs[0].state.layout).toBeInstanceOf(DefaultGridLayoutManager);
});
@@ -82,8 +79,7 @@ describe('deserialization', () => {
],
},
};
const serializer = new TabsLayoutSerializer();
const deserialized = serializer.deserialize(layout, {}, false);
const deserialized = deserializeTabsLayout(layout, {}, false);
expect(deserialized).toBeInstanceOf(TabsLayoutManager);
expect(deserialized.state.tabs[0].state.layout).toBeInstanceOf(AutoGridLayoutManager);
expect(deserialized.state.tabs[1].state.layout).toBeInstanceOf(DefaultGridLayoutManager);
@@ -96,8 +92,7 @@ describe('deserialization', () => {
tabs: [],
},
};
const serializer = new TabsLayoutSerializer();
const deserialized = serializer.deserialize(layout, {}, false);
const deserialized = deserializeTabsLayout(layout, {}, false);
expect(deserialized).toBeInstanceOf(TabsLayoutManager);
expect(deserialized.state.tabs).toHaveLength(0);
});
@@ -2,45 +2,41 @@ import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alp
import { TabItem } from '../../scene/layout-tabs/TabItem';
import { TabsLayoutManager } from '../../scene/layout-tabs/TabsLayoutManager';
import { LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager';
import { layoutSerializerRegistry } from './layoutSerializerRegistry';
import { getLayout } from './utils';
import { layoutDeserializerRegistry } from './layoutSerializerRegistry';
export class TabsLayoutSerializer implements LayoutManagerSerializer {
serialize(layoutManager: TabsLayoutManager): DashboardV2Spec['layout'] {
return {
kind: 'TabsLayout',
spec: {
tabs: layoutManager.state.tabs.map((tab) => {
const layout = getLayout(tab.state.layout);
return {
kind: 'TabsLayoutTab',
spec: {
title: tab.state.title,
layout: layout,
},
};
}),
},
};
export function serializeTabsLayout(layoutManager: TabsLayoutManager): DashboardV2Spec['layout'] {
return {
kind: 'TabsLayout',
spec: {
tabs: layoutManager.state.tabs.map((tab) => {
const layout = tab.state.layout.serialize();
return {
kind: 'TabsLayoutTab',
spec: {
title: tab.state.title,
layout: layout,
},
};
}),
},
};
}
export function deserializeTabsLayout(
layout: DashboardV2Spec['layout'],
elements: DashboardV2Spec['elements'],
preload: boolean
): TabsLayoutManager {
if (layout.kind !== 'TabsLayout') {
throw new Error('Invalid layout kind');
}
deserialize(
layout: DashboardV2Spec['layout'],
elements: DashboardV2Spec['elements'],
preload: boolean
): TabsLayoutManager {
if (layout.kind !== 'TabsLayout') {
throw new Error('Invalid layout kind');
}
const tabs = layout.spec.tabs.map((tab) => {
const layout = tab.spec.layout;
return new TabItem({
title: tab.spec.title,
layout: layoutSerializerRegistry.get(layout.kind).serializer.deserialize(layout, elements, preload),
});
const tabs = layout.spec.tabs.map((tab) => {
const layout = tab.spec.layout;
return new TabItem({
title: tab.spec.title,
layout: layoutDeserializerRegistry.get(layout.kind).deserialize(layout, elements, preload),
});
return new TabsLayoutManager({ tabs });
}
});
return new TabsLayoutManager({ tabs });
}
@@ -1,22 +1,43 @@
import { Registry, RegistryItem } from '@grafana/data';
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager';
import { DashboardLayoutManager } from '../../scene/types/DashboardLayoutManager';
import { DefaultGridLayoutManagerSerializer } from './DefaultGridLayoutSerializer';
import { AutoGridLayoutSerializer } from './ResponsiveGridLayoutSerializer';
import { RowsLayoutSerializer } from './RowsLayoutSerializer';
import { TabsLayoutSerializer } from './TabsLayoutSerializer';
import { deserializeDefaultGridLayout } from './DefaultGridLayoutSerializer';
import { deserializeAutoGridLayout } from './ResponsiveGridLayoutSerializer';
import { deserializeRowsLayout } from './RowsLayoutSerializer';
import { deserializeTabsLayout } from './TabsLayoutSerializer';
interface LayoutSerializerRegistryItem extends RegistryItem {
serializer: LayoutManagerSerializer;
deserialize: (
layout: DashboardV2Spec['layout'],
elements: DashboardV2Spec['elements'],
preload: boolean
) => DashboardLayoutManager;
}
export const layoutSerializerRegistry: Registry<LayoutSerializerRegistryItem> =
export const layoutDeserializerRegistry: Registry<LayoutSerializerRegistryItem> =
new Registry<LayoutSerializerRegistryItem>(() => {
return [
{ id: 'GridLayout', name: 'Grid Layout', serializer: new DefaultGridLayoutManagerSerializer() },
{ id: 'AutoGridLayout', name: 'Auto Grid Layout', serializer: new AutoGridLayoutSerializer() },
{ id: 'RowsLayout', name: 'Rows Layout', serializer: new RowsLayoutSerializer() },
{ id: 'TabsLayout', name: 'Tabs Layout', serializer: new TabsLayoutSerializer() },
{
id: 'GridLayout',
name: 'Grid Layout',
deserialize: deserializeDefaultGridLayout,
},
{
id: 'AutoGridLayout',
name: 'Auto Grid Layout',
deserialize: deserializeAutoGridLayout,
},
{
id: 'RowsLayout',
name: 'Rows Layout',
deserialize: deserializeRowsLayout,
},
{
id: 'TabsLayout',
name: 'Tabs Layout',
deserialize: deserializeTabsLayout,
},
];
});
@@ -36,8 +36,6 @@ import { DashboardLayoutManager } from '../../scene/types/DashboardLayoutManager
import { getVizPanelKeyForPanelId } from '../../utils/utils';
import { transformMappingsToV1 } from '../transformToV1TypesUtils';
import { layoutSerializerRegistry } from './layoutSerializerRegistry';
export function buildVizPanel(panel: PanelKind): VizPanel {
const titleItems: SceneObject[] = [];
@@ -227,12 +225,7 @@ function panelQueryKindToSceneQuery(query: PanelQueryKind): SceneDataQuery {
}
export function getLayout(sceneState: DashboardLayoutManager): DashboardV2Spec['layout'] {
const registryItem = layoutSerializerRegistry.get(sceneState.descriptor.kind ?? '');
if (!registryItem) {
throw new Error(`Layout serializer not found for kind: ${sceneState.descriptor.kind}`);
}
return registryItem.serializer.serialize(sceneState);
return sceneState.serialize();
}
export function getConditionalRendering(item: RowsLayoutRowKind | AutoGridLayoutItemKind): ConditionalRendering {
@@ -527,7 +527,7 @@ describe('transformSaveModelSchemaV2ToScene', () => {
};
const scene = transformSaveModelSchemaV2ToScene(dashboard);
const layoutManager = scene.state.body as AutoGridLayoutManager;
expect(layoutManager.descriptor.kind).toBe('AutoGridLayout');
expect(layoutManager.descriptor.id).toBe('auto-grid');
expect(layoutManager.state.maxColumnCount).toBe(4);
expect(layoutManager.state.columnWidth).toBe(100);
expect(layoutManager.state.rowHeight).toBe('standard');
@@ -572,7 +572,7 @@ describe('transformSaveModelSchemaV2ToScene', () => {
};
const scene = transformSaveModelSchemaV2ToScene(dashboard);
const layoutManager = scene.state.body as TabsLayoutManager;
expect(layoutManager.descriptor.kind).toBe('TabsLayout');
expect(layoutManager.descriptor.id).toBe('tabs-layout');
expect(layoutManager.state.tabs.length).toBe(1);
expect(layoutManager.state.tabs[0].state.title).toBe('tab1');
const gridLayoutManager = layoutManager.state.tabs[0].state.layout as AutoGridLayoutManager;
@@ -648,10 +648,10 @@ describe('transformSaveModelSchemaV2ToScene', () => {
};
const scene = transformSaveModelSchemaV2ToScene(dashboard);
const layoutManager = scene.state.body as RowsLayoutManager;
expect(layoutManager.descriptor.kind).toBe('RowsLayout');
expect(layoutManager.descriptor.id).toBe('rows-layout');
expect(layoutManager.state.rows.length).toBe(2);
const row1Manager = layoutManager.state.rows[0].state.layout as AutoGridLayoutManager;
expect(row1Manager.descriptor.kind).toBe('AutoGridLayout');
expect(row1Manager.descriptor.id).toBe('auto-grid');
expect(row1Manager.state.maxColumnCount).toBe(4);
expect(row1Manager.state.columnWidth).toBe('standard');
expect(row1Manager.state.rowHeight).toBe('standard');
@@ -659,7 +659,7 @@ describe('transformSaveModelSchemaV2ToScene', () => {
expect(row1GridItem.state.body.state.key).toBe('panel-1');
const row2Manager = layoutManager.state.rows[1].state.layout as DefaultGridLayoutManager;
expect(row2Manager.descriptor.kind).toBe('GridLayout');
expect(row2Manager.descriptor.id).toBe('default-grid');
const row2GridItem = row2Manager.state.grid.state.children[0] as SceneGridItem;
expect(row2GridItem.state.body!.state.key).toBe('panel-2');
});
@@ -63,7 +63,7 @@ import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSes
import { getIntervalsFromQueryString } from '../utils/utils';
import { SnapshotVariable } from './custom-variables/SnapshotVariable';
import { layoutSerializerRegistry } from './layoutSerializers/layoutSerializerRegistry';
import { layoutDeserializerRegistry } from './layoutSerializers/layoutSerializerRegistry';
import { getRuntimeVariableDataSource } from './layoutSerializers/utils';
import { registerPanelInteractionsReporter } from './transformSaveModelToScene';
import {
@@ -142,9 +142,9 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
meta.canSave = false;
}
const layoutManager: DashboardLayoutManager = layoutSerializerRegistry
const layoutManager: DashboardLayoutManager = layoutDeserializerRegistry
.get(dashboard.layout.kind)
.serializer.deserialize(dashboard.layout, dashboard.elements, dashboard.preload);
.deserialize(dashboard.layout, dashboard.elements, dashboard.preload);
//createLayoutManager(dashboard);
@@ -52,7 +52,6 @@ import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getLibraryPanelBehavior, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils';
import { DSReferencesMapping } from './DashboardSceneSerializer';
import { getLayout } from './layoutSerializers/utils';
import { sceneVariablesSetToSchemaV2Variables } from './sceneVariablesSetToVariables';
import { colorIdEnumToColorIdV2, transformCursorSynctoEnum } from './transformToV2TypesUtils';
@@ -112,7 +111,7 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps
// EOF annotations
// layout
layout: getLayout(sceneDash.body),
layout: sceneDash.body.serialize(),
// EOF layout
};