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
This commit is contained in:
Oscar Kilhed
2025-02-20 10:17:05 +01:00
committed by GitHub
parent 30aa676724
commit 4dfb21ecdd
6 changed files with 107 additions and 2 deletions
@@ -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');
});
});
});
@@ -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 };
}
@@ -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);
@@ -77,6 +77,10 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> 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()] });
}
@@ -47,6 +47,10 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> 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()) {
@@ -80,7 +80,7 @@ export interface DashboardLayoutManager<S = {}> extends SceneObject {
* @param ancestorKey
* @param isSource
*/
cloneLayout?(ancestorKey: string, isSource: boolean): DashboardLayoutManager;
cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager;
}
export interface LayoutManagerSerializer {