From 38c96c4c7d8ad800bc6d5eec8dac9077472b0fed Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Thu, 27 Mar 2025 17:25:53 +0100 Subject: [PATCH] Dynamic dashboards: Refactor serializing and deserializing layout managers to simplify (#103008) Refactor serializing and deserializing layout managers to simplify --- .../DefaultGridLayoutManager.tsx | 7 +- .../ResponsiveGridLayoutManager.tsx | 7 +- .../scene/layout-rows/RowsLayoutManager.tsx | 7 +- .../scene/layout-tabs/TabsLayoutManager.tsx | 7 +- .../scene/types/DashboardLayoutManager.ts | 5 + .../scene/types/LayoutRegistryItem.ts | 14 +- .../DefaultGridLayoutSerializer.ts | 48 ++--- .../ResponsiveGridLayoutSerializer.ts | 168 +++++++++--------- .../RowsLayoutSerializer.test.ts | 32 ++-- .../layoutSerializers/RowsLayoutSerializer.ts | 119 ++++++------- .../TabsLayoutSerializer.test.ts | 17 +- .../layoutSerializers/TabsLayoutSerializer.ts | 70 ++++---- .../layoutSerializerRegistry.ts | 43 +++-- .../serialization/layoutSerializers/utils.ts | 9 +- .../transformSaveModelSchemaV2ToScene.test.ts | 10 +- .../transformSaveModelSchemaV2ToScene.ts | 6 +- .../transformSceneToSaveModelSchemaV2.ts | 3 +- 17 files changed, 288 insertions(+), 284 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx index ad90fe94f51..3fcb85b6fe6 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -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) { diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx index 14a6cfb6b7b..097aa617f58 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx @@ -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) { diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx index 41d65fdea75..53eaccdad3e 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -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 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) { diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx index 77829bbbd4c..c1c69c26bea 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx @@ -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 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()] }); diff --git a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts index 1065b592f67..ff73cdffdcb 100644 --- a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts +++ b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts @@ -17,6 +17,11 @@ export interface DashboardLayoutManager extends SceneObject { */ descriptor: Readonly; + /** + * Serializer for layout + */ + serialize(): DashboardV2Spec['layout']; + /** * Adds a new panel to the layout */ diff --git a/public/app/features/dashboard-scene/scene/types/LayoutRegistryItem.ts b/public/app/features/dashboard-scene/scene/types/LayoutRegistryItem.ts index 09445d450f5..cd40b7f3e6d 100644 --- a/public/app/features/dashboard-scene/scene/types/LayoutRegistryItem.ts +++ b/public/app/features/dashboard-scene/scene/types/LayoutRegistryItem.ts @@ -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 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) */ diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts index 75a72636172..12d4add0286 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/DefaultGridLayoutSerializer.ts @@ -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( diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/ResponsiveGridLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/ResponsiveGridLayoutSerializer.ts index 36cc352dc78..4fe3e05a9e0 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/ResponsiveGridLayoutSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/ResponsiveGridLayoutSerializer.ts @@ -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) { 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 98e4988a4ca..efa61002547 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.test.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.test.ts @@ -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', diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts index 0f06109d0e1..2d0458d702d 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts @@ -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 }); } diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.test.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.test.ts index d2494056abd..38bb91ce996 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.test.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.test.ts @@ -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); }); diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts index c3f7eedc9db..729aaa83ec2 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts @@ -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 }); } diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/layoutSerializerRegistry.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/layoutSerializerRegistry.ts index 72f43d00e0a..b0d8bd79a7b 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/layoutSerializerRegistry.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/layoutSerializerRegistry.ts @@ -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 = +export const layoutDeserializerRegistry: Registry = new Registry(() => { 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, + }, ]; }); diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts index d908aa2ba18..c6b78dd6fb1 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts @@ -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 { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts index 85fb8ccd082..552921bd882 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.test.ts @@ -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'); }); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 78e21872b0b..4899d5a067f 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -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