From 4dfb21ecdddc7dca54fc1f564efd850fbf9d8aa9 Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Thu, 20 Feb 2025 10:17:05 +0100 Subject: [PATCH] Dynamic dashboards: Implement cloneLayout for responsive grid (#100855) * implement clone layout for responsive grid * Add tests * Fix lint * Make sure we base new keys on old keys * Start tests from panel 1 instead of 0 --- .../DefaultGridLayoutManager.test.tsx | 16 ++++++ .../ResponsiveGridLayoutManager.test.ts | 53 +++++++++++++++++++ .../ResponsiveGridLayoutManager.tsx | 30 ++++++++++- .../scene/layout-rows/RowsLayoutManager.tsx | 4 ++ .../scene/layout-tabs/TabsLayoutManager.tsx | 4 ++ .../scene/types/DashboardLayoutManager.ts | 2 +- 6 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.test.ts diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx index fdb9dd9fc38..c2e3bb77691 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx @@ -210,6 +210,22 @@ describe('DefaultGridLayoutManager', () => { expect(gridRow.state.children.length).toBe(3); }); + + it('Should clone the layout correctly', () => { + const { manager } = setup(); + const clone = manager.cloneLayout('foo', true) as DefaultGridLayoutManager; + const panelA = findVizPanelByKey(clone, 'foo/grid-item-0/panel-0'); + expect(panelA?.state.title).toBe('Panel A'); + + const panelB = findVizPanelByKey(clone, 'foo/grid-item-1/panel-1'); + expect(panelB?.state.title).toBe('Panel B'); + + const panelC = findVizPanelByKey(clone, 'foo/panel-2/grid-item-3/panel-3'); + expect(panelC?.state.title).toBe('Panel C'); + + const panelD = findVizPanelByKey(clone, 'foo/panel-2/grid-item-4/panel-4'); + expect(panelD?.state.title).toBe('Panel D'); + }); }); }); diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.test.ts b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.test.ts new file mode 100644 index 00000000000..ff43c9b1e8f --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.test.ts @@ -0,0 +1,53 @@ +import { SceneCSSGridLayout, SceneQueryRunner, VizPanel } from '@grafana/scenes'; + +import { findVizPanelByKey } from '../../utils/utils'; +import { DashboardScene } from '../DashboardScene'; + +import { ResponsiveGridItem } from './ResponsiveGridItem'; +import { ResponsiveGridLayoutManager } from './ResponsiveGridLayoutManager'; + +describe('ResponsiveGridLayoutManager', () => { + it('Should clone the layout', () => { + const { manager } = setup(); + const clone = manager.cloneLayout('foo', true) as ResponsiveGridLayoutManager; + + expect(clone).not.toBe(manager); + expect(clone.state.layout).not.toBe(manager.state.layout); + expect(clone.state.layout.state.children).not.toBe(manager.state.layout.state.children); + expect(clone.state.layout.state.children.length).toBe(manager.state.layout.state.children.length); + + const panelA = findVizPanelByKey(clone, 'foo/grid-item-1/panel-1'); + expect(panelA?.state.title).toBe('Panel A'); + + const panelB = findVizPanelByKey(clone, 'foo/grid-item-2/panel-2'); + expect(panelB?.state.title).toBe('Panel B'); + }); +}); + +function setup() { + const gridItems = [ + new ResponsiveGridItem({ + key: 'grid-item-1', + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }), + }), + new ResponsiveGridItem({ + key: 'grid-item-2', + body: new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', + }), + }), + ]; + + const manager = new ResponsiveGridLayoutManager({ layout: new SceneCSSGridLayout({ children: gridItems }) }); + + new DashboardScene({ body: manager }); + + return { manager }; +} 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 bde10fa3374..8c3505038c2 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 @@ -2,8 +2,14 @@ import { SceneComponentProps, SceneCSSGridLayout, SceneObjectBase, SceneObjectSt import { t } from 'app/core/internationalization'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; +import { joinCloneKeys } from '../../utils/clone'; import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph'; -import { getDashboardSceneFor, getGridItemKeyForPanelId, getVizPanelKeyForPanelId } from '../../utils/utils'; +import { + getDashboardSceneFor, + getGridItemKeyForPanelId, + getPanelIdForVizPanel, + getVizPanelKeyForPanelId, +} from '../../utils/utils'; import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; @@ -115,6 +121,28 @@ export class ResponsiveGridLayoutManager return false; } + public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager { + return this.clone({ + layout: this.state.layout.clone({ + children: this.state.layout.state.children.map((gridItem) => { + if (gridItem instanceof ResponsiveGridItem) { + // Get the original panel ID from the gridItem's key + const panelId = getPanelIdForVizPanel(gridItem.state.body); + const gridItemKey = joinCloneKeys(ancestorKey, getGridItemKeyForPanelId(panelId)); + + return gridItem.clone({ + key: gridItemKey, + body: gridItem.state.body.clone({ + key: joinCloneKeys(gridItemKey, getVizPanelKeyForPanelId(panelId)), + }), + }); + } + throw new Error('Unexpected child type'); + }), + }), + }); + } + public addNewRow() { const shouldAddRow = this.hasVizPanels(); const rowsLayout = RowsLayoutManager.createFromLayout(this); 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 fd3150af3e8..54fb186937d 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -77,6 +77,10 @@ export class RowsLayoutManager extends SceneObjectBase i return false; } + public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager { + throw new Error('Method not implemented.'); + } + public addNewRow() { this.setState({ rows: [...this.state.rows, new RowItem()] }); } 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 62279b315c1..daa86286dae 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx @@ -47,6 +47,10 @@ export class TabsLayoutManager extends SceneObjectBase i return panels; } + public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager { + throw new Error('Method not implemented.'); + } + public hasVizPanels(): boolean { for (const tab of this.state.tabs) { if (tab.getLayout().hasVizPanels()) { diff --git a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts index 18fc2c74064..6041799d8db 100644 --- a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts +++ b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts @@ -80,7 +80,7 @@ export interface DashboardLayoutManager extends SceneObject { * @param ancestorKey * @param isSource */ - cloneLayout?(ancestorKey: string, isSource: boolean): DashboardLayoutManager; + cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager; } export interface LayoutManagerSerializer {