From d269b4bf0dd74613e1c5ea0b783c90c0f91df2c9 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:47:18 +0200 Subject: [PATCH] Scenes: Copy/paste library panels (#83962) * Scenes: Copy/paste library panels * more tests --- .../scene/DashboardScene.test.tsx | 101 ++++++++++++++++-- .../dashboard-scene/scene/DashboardScene.tsx | 35 +++++- 2 files changed, 125 insertions(+), 11 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 51fbbbc8748..fd938520442 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -17,7 +17,11 @@ import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { VariablesChanged } from 'app/features/variables/types'; import { createWorker } from '../saving/createDetectChangesWorker'; -import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; +import { + buildGridItemForLibPanel, + buildGridItemForPanel, + transformSaveModelToScene, +} from '../serialization/transformSaveModelToScene'; import { DecoratedRevisionModel } from '../settings/VersionsEditView'; import { historySrv } from '../settings/version-history/HistorySrv'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; @@ -25,6 +29,7 @@ import { djb2Hash } from '../utils/djb2Hash'; import { DashboardControls } from './DashboardControls'; import { DashboardScene, DashboardSceneState } from './DashboardScene'; +import { LibraryVizPanel } from './LibraryVizPanel'; jest.mock('../settings/version-history/HistorySrv'); jest.mock('../serialization/transformSaveModelToScene'); @@ -244,7 +249,7 @@ describe('DashboardScene', () => { const body = scene.state.body as SceneGridLayout; const gridItem = body.state.children[0] as SceneGridItem; - expect(body.state.children.length).toBe(5); + expect(body.state.children.length).toBe(6); expect(gridItem.state.body!.state.key).toBe('panel-5'); expect(gridItem.state.y).toBe(0); }); @@ -255,7 +260,7 @@ describe('DashboardScene', () => { const body = scene.state.body as SceneGridLayout; const gridItem = body.state.children[0] as SceneGridItem; - expect(body.state.children.length).toBe(5); + expect(body.state.children.length).toBe(6); expect(gridItem.state.body!.state.key).toBe('panel-5'); }); @@ -265,7 +270,7 @@ describe('DashboardScene', () => { const body = scene.state.body as SceneGridLayout; const gridRow = body.state.children[0] as SceneGridRow; - expect(body.state.children.length).toBe(3); + expect(body.state.children.length).toBe(4); expect(gridRow.state.key).toBe('panel-5'); expect(gridRow.state.children[0].state.key).toBe('griditem-1'); expect(gridRow.state.children[1].state.key).toBe('griditem-2'); @@ -313,7 +318,7 @@ describe('DashboardScene', () => { const body = scene.state.body as SceneGridLayout; const gridRow = body.state.children[0] as SceneGridRow; - expect(body.state.children.length).toBe(4); + expect(body.state.children.length).toBe(5); expect(gridRow.state.children.length).toBe(0); }); @@ -333,6 +338,36 @@ describe('DashboardScene', () => { expect(gridRow.state.children.length).toBe(0); }); + it('Should fail to copy a panel if it does not have a grid item parent', () => { + const vizPanel = new VizPanel({ + title: 'Panel Title', + key: 'panel-5', + pluginId: 'timeseries', + }); + + scene.copyPanel(vizPanel); + + expect(scene.state.hasCopiedPanel).toBe(false); + }); + + it('Should fail to copy a library panel if it does not have a grid item parent', () => { + const libVizPanel = new LibraryVizPanel({ + uid: 'uid', + name: 'libraryPanel', + panelKey: 'panel-4', + title: 'Library Panel', + panel: new VizPanel({ + title: 'Library Panel', + key: 'panel-4', + pluginId: 'table', + }), + }); + + scene.copyPanel(libVizPanel.state.panel as VizPanel); + + expect(scene.state.hasCopiedPanel).toBe(false); + }); + it('Should copy a panel', () => { const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as SceneGridItem).state.body; scene.copyPanel(vizPanel as VizPanel); @@ -340,6 +375,15 @@ describe('DashboardScene', () => { expect(scene.state.hasCopiedPanel).toBe(true); }); + it('Should copy a library viz panel', () => { + const libVizPanel = ((scene.state.body as SceneGridLayout).state.children[4] as SceneGridItem).state + .body as LibraryVizPanel; + + scene.copyPanel(libVizPanel.state.panel as VizPanel); + + expect(scene.state.hasCopiedPanel).toBe(true); + }); + it('Should paste a panel', () => { scene.setState({ hasCopiedPanel: true }); jest.spyOn(JSON, 'parse').mockReturnThis(); @@ -359,19 +403,49 @@ describe('DashboardScene', () => { const body = scene.state.body as SceneGridLayout; const gridItem = body.state.children[0] as SceneGridItem; - expect(body.state.children.length).toBe(5); + expect(buildGridItemForPanel).toHaveBeenCalledTimes(1); + expect(body.state.children.length).toBe(6); expect(gridItem.state.body!.state.key).toBe('panel-5'); expect(gridItem.state.y).toBe(0); expect(scene.state.hasCopiedPanel).toBe(false); }); + it('Should paste a library viz panel', () => { + scene.setState({ hasCopiedPanel: true }); + jest.spyOn(JSON, 'parse').mockReturnValue({ libraryPanel: { uid: 'uid', name: 'libraryPanel' } }); + jest.mocked(buildGridItemForLibPanel).mockReturnValue( + new SceneGridItem({ + body: new LibraryVizPanel({ + title: 'Library Panel', + uid: 'uid', + name: 'libraryPanel', + panelKey: 'panel-4', + }), + }) + ); + + scene.pastePanel(); + + const body = scene.state.body as SceneGridLayout; + const gridItem = body.state.children[0] as SceneGridItem; + + const libVizPanel = gridItem.state.body as LibraryVizPanel; + + expect(buildGridItemForLibPanel).toHaveBeenCalledTimes(1); + expect(body.state.children.length).toBe(6); + expect(libVizPanel.state.panelKey).toBe('panel-5'); + expect(libVizPanel.state.panel?.state.key).toBe('panel-5'); + expect(gridItem.state.y).toBe(0); + expect(scene.state.hasCopiedPanel).toBe(false); + }); + it('Should create a new add library panel widget', () => { scene.onCreateLibPanelWidget(); const body = scene.state.body as SceneGridLayout; const gridItem = body.state.children[0] as SceneGridItem; - expect(body.state.children.length).toBe(5); + expect(body.state.children.length).toBe(6); expect(gridItem.state.body!.state.key).toBe('panel-5'); expect(gridItem.state.y).toBe(0); }); @@ -575,6 +649,19 @@ function buildTestScene(overrides?: Partial) { $data: new SceneQueryRunner({ key: 'data-query-runner2', queries: [{ refId: 'A' }] }), }), }), + new SceneGridItem({ + body: new LibraryVizPanel({ + uid: 'uid', + name: 'libraryPanel', + panelKey: 'panel-4', + title: 'Library Panel', + panel: new VizPanel({ + title: 'Library Panel', + key: 'panel-4', + pluginId: 'table', + }), + }), + }), ], }), ...overrides, diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 51c6eec0c03..c5ddb5ecfe5 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -33,7 +33,11 @@ import { PanelEditor } from '../panel-edit/PanelEditor'; import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker'; import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer'; import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer'; -import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; +import { + buildGridItemForLibPanel, + buildGridItemForPanel, + transformSaveModelToScene, +} from '../serialization/transformSaveModelToScene'; import { gridItemToPanel } from '../serialization/transformSceneToSaveModel'; import { DecoratedRevisionModel } from '../settings/VersionsEditView'; import { DashboardEditView } from '../settings/utils'; @@ -57,6 +61,7 @@ import { import { AddLibraryPanelWidget } from './AddLibraryPanelWidget'; import { DashboardControls } from './DashboardControls'; import { DashboardSceneUrlSync } from './DashboardSceneUrlSync'; +import { LibraryVizPanel } from './LibraryVizPanel'; import { PanelRepeaterGridItem } from './PanelRepeaterGridItem'; import { ViewPanelScene } from './ViewPanelScene'; import { setupKeyboardShortcuts } from './keyboardShortcuts'; @@ -480,7 +485,17 @@ export class DashboardScene extends SceneObjectBase { return; } - const gridItem = vizPanel.parent; + let gridItem = vizPanel.parent; + + if (vizPanel.parent instanceof LibraryVizPanel) { + const libraryVizPanel = vizPanel.parent; + + if (!libraryVizPanel.parent) { + return; + } + + gridItem = libraryVizPanel.parent; + } const jsonData = gridItemToPanel(gridItem); @@ -497,8 +512,10 @@ export class DashboardScene extends SceneObjectBase { const jsonData = store.get(LS_PANEL_COPY_KEY); const jsonObj = JSON.parse(jsonData); const panelModel = new PanelModel(jsonObj); + const gridItem = !panelModel.libraryPanel + ? buildGridItemForPanel(panelModel) + : buildGridItemForLibPanel(panelModel); - const gridItem = buildGridItemForPanel(panelModel); const sceneGridLayout = this.state.body; if (!(gridItem instanceof SceneGridItem) && !(gridItem instanceof PanelRepeaterGridItem)) { @@ -507,7 +524,17 @@ export class DashboardScene extends SceneObjectBase { const panelId = dashboardSceneGraph.getNextPanelId(this); - if (gridItem instanceof SceneGridItem && gridItem.state.body) { + if (gridItem instanceof SceneGridItem && gridItem.state.body instanceof LibraryVizPanel) { + const panelKey = getVizPanelKeyForPanelId(panelId); + + gridItem.state.body.setState({ panelKey }); + + const vizPanel = gridItem.state.body.state.panel; + + if (vizPanel instanceof VizPanel) { + vizPanel.setState({ key: panelKey }); + } + } else if (gridItem instanceof SceneGridItem && gridItem.state.body) { gridItem.state.body.setState({ key: getVizPanelKeyForPanelId(panelId), });