diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 807c7f7bce1..4ffd7472048 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -30,6 +30,7 @@ import { djb2Hash } from '../utils/djb2Hash'; import { DashboardControls } from './DashboardControls'; import { DashboardScene, DashboardSceneState } from './DashboardScene'; import { LibraryVizPanel } from './LibraryVizPanel'; +import { PanelRepeaterGridItem } from './PanelRepeaterGridItem'; jest.mock('../settings/version-history/HistorySrv'); jest.mock('../serialization/transformSaveModelToScene'); @@ -541,7 +542,119 @@ describe('DashboardScene', () => { expect(gridRow.state.children.length).toBe(1); }); - it('Should unlink a library panel', () => { + it('Should duplicate a panel', () => { + const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as SceneGridItem).state.body; + scene.duplicatePanel(vizPanel as VizPanel); + + const body = scene.state.body as SceneGridLayout; + const gridItem = body.state.children[5] as SceneGridItem; + + expect(body.state.children.length).toBe(6); + expect(gridItem.state.body!.state.key).toBe('panel-7'); + }); + + it('Should duplicate a library panel', () => { + const libraryPanel = ((scene.state.body as SceneGridLayout).state.children[4] as SceneGridItem).state.body; + const vizPanel = (libraryPanel as LibraryVizPanel).state.panel; + scene.duplicatePanel(vizPanel as VizPanel); + + const body = scene.state.body as SceneGridLayout; + const gridItem = body.state.children[5] as SceneGridItem; + + const libVizPanel = gridItem.state.body as LibraryVizPanel; + + expect(body.state.children.length).toBe(6); + expect(libVizPanel.state.panelKey).toBe('panel-7'); + expect(libVizPanel.state.panel?.state.key).toBe('panel-7'); + }); + + it('Should duplicate a repeated panel', () => { + const scene = buildTestScene({ + body: new SceneGridLayout({ + children: [ + new PanelRepeaterGridItem({ + key: `grid-item-1`, + width: 24, + height: 8, + repeatedPanels: [ + new VizPanel({ + title: 'Library Panel', + key: 'panel-1', + pluginId: 'table', + }), + ], + source: new VizPanel({ + title: 'Library Panel', + key: 'panel-1', + pluginId: 'table', + }), + variableName: 'custom', + }), + ], + }), + }); + + const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as PanelRepeaterGridItem).state + .repeatedPanels![0]; + + scene.duplicatePanel(vizPanel as VizPanel); + + const body = scene.state.body as SceneGridLayout; + const gridItem = body.state.children[1] as SceneGridItem; + + expect(body.state.children.length).toBe(2); + expect(gridItem.state.body!.state.key).toBe('panel-2'); + }); + + it('Should duplicate a panel in a row', () => { + const vizPanel = ( + ((scene.state.body as SceneGridLayout).state.children[2] as SceneGridRow).state.children[0] as SceneGridItem + ).state.body; + scene.duplicatePanel(vizPanel as VizPanel); + + const body = scene.state.body as SceneGridLayout; + const gridRow = body.state.children[2] as SceneGridRow; + const gridItem = gridRow.state.children[2] as SceneGridItem; + + expect(gridRow.state.children.length).toBe(3); + expect(gridItem.state.body!.state.key).toBe('panel-7'); + }); + + it('Should duplicate a library panel in a row', () => { + const libraryPanel = ( + ((scene.state.body as SceneGridLayout).state.children[2] as SceneGridRow).state.children[1] as SceneGridItem + ).state.body; + const vizPanel = (libraryPanel as LibraryVizPanel).state.panel; + + scene.duplicatePanel(vizPanel as VizPanel); + + const body = scene.state.body as SceneGridLayout; + const gridRow = body.state.children[2] as SceneGridRow; + const gridItem = gridRow.state.children[2] as SceneGridItem; + + const libVizPanel = gridItem.state.body as LibraryVizPanel; + + expect(gridRow.state.children.length).toBe(3); + expect(libVizPanel.state.panelKey).toBe('panel-7'); + expect(libVizPanel.state.panel?.state.key).toBe('panel-7'); + }); + + it('Should fail to duplicate a panel if it does not have a grid item parent', () => { + const vizPanel = new VizPanel({ + title: 'Panel Title', + key: 'panel-5', + pluginId: 'timeseries', + }); + + scene.duplicatePanel(vizPanel); + + const body = scene.state.body as SceneGridLayout; + + // length remains unchanged + expect(body.state.children.length).toBe(5); + }); + + it('Should unlink a library panel', () => { const libPanel = new LibraryVizPanel({ title: 'title', uid: 'abc', diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 387071fc1d9..095115da308 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -43,7 +43,7 @@ import { DecoratedRevisionModel } from '../settings/VersionsEditView'; import { DashboardEditView } from '../settings/utils'; import { historySrv } from '../settings/version-history'; import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper'; -import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; +import { dashboardSceneGraph, getLibraryVizPanelFromVizPanel } from '../utils/dashboardSceneGraph'; import { djb2Hash } from '../utils/djb2Hash'; import { getDashboardUrl } from '../utils/urlBuilders'; import { @@ -459,7 +459,9 @@ export class DashboardScene extends SceneObjectBase { return; } - const gridItem = vizPanel.parent; + const libraryPanel = getLibraryVizPanelFromVizPanel(vizPanel); + + const gridItem = libraryPanel ? libraryPanel.parent : vizPanel.parent; if (!(gridItem instanceof SceneGridItem || gridItem instanceof PanelRepeaterGridItem)) { console.error('Trying to duplicate a panel in a layout that is not SceneGridItem or PanelRepeaterGridItem'); @@ -468,26 +470,45 @@ export class DashboardScene extends SceneObjectBase { let panelState; let panelData; - if (gridItem instanceof PanelRepeaterGridItem) { - const { key, ...gridRepeaterSourceState } = sceneUtils.cloneSceneObjectState(gridItem.state.source.state); - panelState = { ...gridRepeaterSourceState }; - panelData = sceneGraph.getData(gridItem.state.source).clone(); + let newGridItem; + const newPanelId = dashboardSceneGraph.getNextPanelId(this); + + if (libraryPanel) { + const gridItemToDuplicateState = sceneUtils.cloneSceneObjectState(gridItem.state); + + newGridItem = new SceneGridItem({ + x: gridItemToDuplicateState.x, + y: gridItemToDuplicateState.y, + width: gridItemToDuplicateState.width, + height: gridItemToDuplicateState.height, + body: new LibraryVizPanel({ + title: libraryPanel.state.title, + uid: libraryPanel.state.uid, + name: libraryPanel.state.name, + panelKey: getVizPanelKeyForPanelId(newPanelId), + }), + }); } else { - const { key, ...gridItemPanelState } = sceneUtils.cloneSceneObjectState(vizPanel.state); - panelState = { ...gridItemPanelState }; - panelData = sceneGraph.getData(vizPanel).clone(); + if (gridItem instanceof PanelRepeaterGridItem) { + panelState = sceneUtils.cloneSceneObjectState(gridItem.state.source.state); + panelData = sceneGraph.getData(gridItem.state.source).clone(); + } else { + panelState = sceneUtils.cloneSceneObjectState(vizPanel.state); + panelData = sceneGraph.getData(vizPanel).clone(); + } + + // when we duplicate a panel we don't want to clone the alert state + delete panelData.state.data?.alertState; + + newGridItem = new SceneGridItem({ + x: gridItem.state.x, + y: gridItem.state.y, + height: NEW_PANEL_HEIGHT, + width: NEW_PANEL_WIDTH, + body: new VizPanel({ ...panelState, $data: panelData, key: getVizPanelKeyForPanelId(newPanelId) }), + }); } - // when we duplicate a panel we don't want to clone the alert state - delete panelData.state.data?.alertState; - - const { key: gridItemKey, ...gridItemToDuplicateState } = sceneUtils.cloneSceneObjectState(gridItem.state); - - const newGridItem = new SceneGridItem({ - ...gridItemToDuplicateState, - body: new VizPanel({ ...panelState, $data: panelData }), - }); - if (!(this.state.body instanceof SceneGridLayout)) { console.error('Trying to duplicate a panel in a layout that is not SceneGridLayout '); return; @@ -495,6 +516,18 @@ export class DashboardScene extends SceneObjectBase { const sceneGridLayout = this.state.body; + if (gridItem.parent instanceof SceneGridRow) { + const row = gridItem.parent; + + row.setState({ + children: [...row.state.children, newGridItem], + }); + + sceneGridLayout.forceRender(); + + return; + } + sceneGridLayout.setState({ children: [...sceneGridLayout.state.children, newGridItem], }); diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts index ea529d79c17..bc50b5d889f 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts @@ -16,6 +16,7 @@ import { DashboardControls } from '../scene/DashboardControls'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; +import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; import { dashboardSceneGraph, getNextPanelId } from './dashboardSceneGraph'; import { findVizPanelByKey } from './utils'; @@ -123,7 +124,7 @@ describe('dashboardSceneGraph', () => { expect(id).toBe(4); }); - it('should take library panels into account', () => { + it('should take library panels, panels in rows and panel repeaters into account', () => { const scene = buildTestScene({ body: new SceneGridLayout({ children: [ @@ -151,6 +152,17 @@ describe('dashboardSceneGraph', () => { pluginId: 'table', }), }), + new PanelRepeaterGridItem({ + source: new VizPanel({ + title: 'Panel C', + key: 'panel-4', + pluginId: 'table', + }), + variableName: 'repeat', + repeatedPanels: [], + repeatDirection: 'h', + maxPerRow: 1, + }), new SceneGridRow({ key: 'key', title: 'row', @@ -178,7 +190,7 @@ describe('dashboardSceneGraph', () => { const id = getNextPanelId(scene); - expect(id).toBe(4); + expect(id).toBe(5); }); it('should get next panel id in a layout with rows', () => { diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts index 2a289ff47ad..6980a1c5253 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts @@ -11,6 +11,7 @@ import { import { DashboardScene } from '../scene/DashboardScene'; import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { VizPanelLinks } from '../scene/PanelLinks'; +import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; import { getPanelIdForLibraryVizPanel, getPanelIdForVizPanel } from './utils'; @@ -85,6 +86,21 @@ export function getNextPanelId(dashboard: DashboardScene): number { } for (const child of body.state.children) { + if (child instanceof PanelRepeaterGridItem) { + const vizPanel = child.state.source; + + if (vizPanel) { + const panelId = + vizPanel instanceof LibraryVizPanel + ? getPanelIdForLibraryVizPanel(vizPanel) + : getPanelIdForVizPanel(vizPanel); + + if (panelId > max) { + max = panelId; + } + } + } + if (child instanceof SceneGridItem) { const vizPanel = child.state.body; @@ -130,6 +146,19 @@ export function getNextPanelId(dashboard: DashboardScene): number { return max + 1; } +// Returns the LibraryVizPanel that corresponds to the given VizPanel if it exists +export const getLibraryVizPanelFromVizPanel = (vizPanel: VizPanel): LibraryVizPanel | null => { + if (vizPanel.parent instanceof LibraryVizPanel) { + return vizPanel.parent; + } + + if (vizPanel.parent instanceof PanelRepeaterGridItem && vizPanel.parent.state.source instanceof LibraryVizPanel) { + return vizPanel.parent.state.source; + } + + return null; +}; + export const dashboardSceneGraph = { getTimePicker, getRefreshPicker,