From 1941ae21d780e858b59e3a0a9d7d379077de918e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 27 Sep 2024 15:11:28 +0200 Subject: [PATCH] DashboardScene: Refactor body property to be layout manager interface (#93738) * Began layout refactor * fixing tests * Progress * Progress * Progress * Progress * Progress * Progress * finally no errors * Remove unused interface * Remove unused interface * fixed tests * Update * Update * Fixes to keyboard shortcuts and solo route * fix lint issues --- .betterer.results | 4 + .../inspect/HelpWizard/HelpWizard.test.tsx | 17 +- .../HelpWizard/SupportSnapshotService.test.ts | 17 +- .../inspect/InspectJsonTab.test.tsx | 33 +- .../panel-edit/PanelEditor.test.ts | 16 +- .../saving/DashboardPrompt.test.tsx | 26 +- .../scene/AddLibraryPanelDrawer.test.tsx | 113 +--- .../scene/AddLibraryPanelDrawer.tsx | 39 +- .../DashboardDatasourceBehaviour.test.tsx | 180 +----- .../scene/DashboardGridItem.test.tsx | 14 +- .../scene/DashboardScene.test.tsx | 606 +++--------------- .../dashboard-scene/scene/DashboardScene.tsx | 290 +-------- .../scene/DashboardSceneUrlSync.test.ts | 50 +- .../scene/DashboardSceneUrlSync.ts | 21 +- .../scene/LibraryPanelBehavior.test.tsx | 7 +- .../scene/NavToolbarActions.test.tsx | 48 +- .../scene/PanelMenuBehavior.test.tsx | 16 +- .../scene/RowRepeaterBehavior.test.tsx | 3 +- .../scene/ViewPanelScene.test.tsx | 35 +- .../scene/keyboardShortcuts.ts | 9 +- .../DefaultGridLayoutManager.test.tsx | 281 ++++++++ .../DefaultGridLayoutManager.tsx | 355 ++++++++++ .../scene/row-actions/RowActions.tsx | 23 +- .../features/dashboard-scene/scene/types.ts | 88 ++- .../transformSaveModelToScene.test.ts | 14 +- .../transformSaveModelToScene.ts | 11 +- .../transformSceneToSaveModel.test.ts | 6 +- .../transformSceneToSaveModel.ts | 6 +- .../settings/AnnotationsEditView.test.tsx | 5 +- .../settings/DashboardLinksEditView.test.tsx | 5 +- .../settings/GeneralSettingsEditView.test.tsx | 5 +- .../settings/PermissionsEditView.test.tsx | 5 +- .../settings/VariablesEditView.test.tsx | 28 +- .../settings/VersionsEditView.test.tsx | 5 +- .../ExportButton/ExportButton.test.tsx | 17 +- .../sharing/ExportButton/ExportMenu.test.tsx | 17 +- .../sharing/ShareButton/ShareButton.test.tsx | 17 +- .../sharing/ShareButton/ShareMenu.test.tsx | 17 +- .../share-externally/ShareAlerts.test.tsx | 16 +- .../share-externally/ShareExternally.test.tsx | 31 +- .../sharing/ShareDrawer/ShareDrawer.test.tsx | 5 +- .../sharing/ShareLinkTab.test.tsx | 17 +- .../sharing/public-dashboards/utils.ts | 69 +- .../dashboard-scene/solo/useSoloPanel.ts | 29 +- ...DashboardModelCompatibilityWrapper.test.ts | 119 ++-- .../DashboardModelCompatibilityWrapper.ts | 47 +- .../utils/dashboardSceneGraph.test.ts | 251 +------- .../utils/dashboardSceneGraph.ts | 75 +-- .../dashboard-scene/utils/test-utils.ts | 7 +- .../features/dashboard-scene/utils/utils.ts | 13 - 50 files changed, 1211 insertions(+), 1917 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx create mode 100644 public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx diff --git a/.betterer.results b/.betterer.results index 3ec225ceb44..a6c0daa79b8 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2785,6 +2785,10 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], + "public/app/features/dashboard-scene/scene/types.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + ], "public/app/features/dashboard-scene/serialization/angularMigration.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.test.tsx b/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.test.tsx index e217fbd6d35..da8dd9998f7 100644 --- a/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.test.tsx +++ b/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.test.tsx @@ -2,12 +2,12 @@ import { render, screen } from '@testing-library/react'; import { FieldType, getDefaultTimeRange, LoadingState, toDataFrame } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; -import { SceneGridLayout, SceneQueryRunner, SceneTimeRange, VizPanel, VizPanelMenu } from '@grafana/scenes'; +import { SceneQueryRunner, SceneTimeRange, VizPanel, VizPanelMenu } from '@grafana/scenes'; -import { DashboardGridItem } from '../../scene/DashboardGridItem'; import { DashboardScene } from '../../scene/DashboardScene'; import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks'; import { panelMenuBehavior } from '../../scene/PanelMenuBehavior'; +import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; import { HelpWizard } from './HelpWizard'; @@ -67,18 +67,7 @@ async function buildTestScene() { canEdit: true, isEmbedded: false, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), }); await new Promise((r) => setTimeout(r, 1)); diff --git a/public/app/features/dashboard-scene/inspect/HelpWizard/SupportSnapshotService.test.ts b/public/app/features/dashboard-scene/inspect/HelpWizard/SupportSnapshotService.test.ts index 4d1fc427cbd..4ecf7826ae4 100644 --- a/public/app/features/dashboard-scene/inspect/HelpWizard/SupportSnapshotService.test.ts +++ b/public/app/features/dashboard-scene/inspect/HelpWizard/SupportSnapshotService.test.ts @@ -1,10 +1,10 @@ import { FieldType, getDefaultTimeRange, LoadingState, toDataFrame } from '@grafana/data'; -import { SceneGridLayout, SceneQueryRunner, SceneTimeRange, VizPanel, VizPanelMenu } from '@grafana/scenes'; +import { SceneQueryRunner, SceneTimeRange, VizPanel, VizPanelMenu } from '@grafana/scenes'; -import { DashboardGridItem } from '../../scene/DashboardGridItem'; import { DashboardScene } from '../../scene/DashboardScene'; import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks'; import { panelMenuBehavior } from '../../scene/PanelMenuBehavior'; +import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; import { SnapshotTab, SupportSnapshotService } from './SupportSnapshotService'; @@ -114,18 +114,7 @@ async function buildTestScene() { canEdit: true, isEmbedded: false, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), }); await new Promise((r) => setTimeout(r, 1)); diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx index 84e7df9aa7c..ed28bd2c630 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx @@ -12,7 +12,7 @@ import { } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { setPluginImportUtils, setRunRequest } from '@grafana/runtime'; -import { SceneCanvasText, SceneDataTransformer, SceneGridLayout, SceneQueryRunner, VizPanel } from '@grafana/scenes'; +import { SceneCanvasText, SceneDataTransformer, SceneQueryRunner, VizPanel } from '@grafana/scenes'; import * as libpanels from 'app/features/library-panels/state/api'; import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; @@ -20,6 +20,7 @@ import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene } from '../scene/DashboardScene'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel'; import { activateFullSceneTree } from '../utils/test-utils'; import { findVizPanelByKey } from '../utils/utils'; @@ -106,7 +107,7 @@ describe('InspectJsonTab', () => { const { tab } = await buildTestScene(); const obj = JSON.parse(tab.state.jsonText); - expect(obj.gridPos).toEqual({ x: 0, y: 0, w: 10, h: 12 }); + expect(obj.gridPos).toEqual({ x: 0, y: 0, w: 8, h: 10 }); expect(tab.isEditable()).toBe(true); }); @@ -114,7 +115,7 @@ describe('InspectJsonTab', () => { const { tab } = await buildTestSceneWithLibraryPanel(); const obj = JSON.parse(tab.state.jsonText); - expect(obj.gridPos).toEqual({ x: 0, y: 0, w: 10, h: 12 }); + expect(obj.gridPos).toEqual({ x: 0, y: 0, w: 8, h: 10 }); expect(obj.type).toEqual('table'); expect(tab.isEditable()).toBe(false); }); @@ -202,18 +203,7 @@ async function buildTestScene() { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), }); activateFullSceneTree(scene); @@ -265,24 +255,13 @@ async function buildTestSceneWithLibraryPanel() { jest.spyOn(libpanels, 'getLibraryPanel').mockResolvedValue({ ...libraryPanelState, ...panel }); - const gridItem = new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: libraryPanel, - }); - const scene = new DashboardScene({ title: 'hello', uid: 'dash-1', meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [gridItem], - }), + body: DefaultGridLayoutManager.fromVizPanels([libraryPanel]), }); activateFullSceneTree(scene); diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts index 8117146a4fc..78370296e0a 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts @@ -21,6 +21,7 @@ import * as libAPI from 'app/features/library-panels/state/api'; import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene } from '../scene/DashboardScene'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel'; import { activateFullSceneTree } from '../utils/test-utils'; import { findVizPanelByKey, getQueryRunnerFor } from '../utils/utils'; @@ -124,7 +125,8 @@ describe('PanelEditor', () => { const { panelEditor, dashboard } = await setup({ isNewPanel: true }); panelEditor.onDiscard(); - expect((dashboard.state.body as SceneGridLayout).state.children.length).toBe(0); + const panels = dashboard.state.body.getVizPanels(); + expect(panels.length).toBe(0); }); it('should discard query runner changes', async () => { @@ -212,8 +214,10 @@ describe('PanelEditor', () => { editPanel: editScene, $timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }), isEditing: true, - body: new SceneGridLayout({ - children: [gridItem], + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [gridItem], + }), }), }); @@ -332,8 +336,10 @@ async function setup(options: SetupOptions = {}) { }), ], }), - body: new SceneGridLayout({ - children: [gridItem], + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [gridItem], + }), }), }); diff --git a/public/app/features/dashboard-scene/saving/DashboardPrompt.test.tsx b/public/app/features/dashboard-scene/saving/DashboardPrompt.test.tsx index 76dc822344f..ac2014b6fd5 100644 --- a/public/app/features/dashboard-scene/saving/DashboardPrompt.test.tsx +++ b/public/app/features/dashboard-scene/saving/DashboardPrompt.test.tsx @@ -1,9 +1,9 @@ -import { SceneGridLayout, SceneQueryRunner, SceneTimeRange, VizPanel, behaviors } from '@grafana/scenes'; +import { SceneQueryRunner, SceneTimeRange, VizPanel, behaviors } from '@grafana/scenes'; import { ContextSrv, setContextSrv } from 'app/core/services/context_srv'; import { DashboardControls } from '../scene/DashboardControls'; -import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; import { ignoreChanges } from './DashboardPrompt'; @@ -136,20 +136,14 @@ function buildTestScene(overrides?: Partial) { }), controls: new DashboardControls({}), $behaviors: [new behaviors.CursorSync({})], - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), - }), - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([ + new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }), + ]), ...overrides, }); diff --git a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx index 90116dec207..f298e7b19f9 100644 --- a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx +++ b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx @@ -1,12 +1,12 @@ -import { SceneGridLayout, SceneGridRow, SceneTimeRange, VizPanel } from '@grafana/scenes'; +import { SceneTimeRange, VizPanel } from '@grafana/scenes'; import { LibraryPanel } from '@grafana/schema/dist/esm/index.gen'; import { activateFullSceneTree } from '../utils/test-utils'; import { AddLibraryPanelDrawer } from './AddLibraryPanelDrawer'; -import { DashboardGridItem } from './DashboardGridItem'; import { DashboardScene } from './DashboardScene'; import { LibraryPanelBehavior } from './LibraryPanelBehavior'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), @@ -41,13 +41,12 @@ describe('AddLibraryPanelWidget', () => { addLibPanelDrawer.onAddLibraryPanel(panelInfo); - const layout = dashboard.state.body as SceneGridLayout; - const gridItem = layout.state.children[0] as DashboardGridItem; + const panels = dashboard.state.body.getVizPanels(); + const panel = panels[0]; - expect(layout.state.children.length).toBe(1); - expect(gridItem.state.body!).toBeInstanceOf(VizPanel); - expect(gridItem.state.body.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior); - expect(gridItem.state.body.state.key).toBe('panel-1'); + expect(panels.length).toBe(1); + expect(panel.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior); + expect(panel.state.key).toBe('panel-1'); }); it('should add library panel from menu and enter edit mode in a dashboard that is not already in edit mode', async () => { @@ -60,9 +59,6 @@ describe('AddLibraryPanelWidget', () => { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [], - }), overlay: drawer, }); @@ -86,24 +82,15 @@ describe('AddLibraryPanelWidget', () => { drawer.onAddLibraryPanel(panelInfo); - const layout = dashboard.state.body as SceneGridLayout; - const gridItem = layout.state.children[0] as DashboardGridItem; + const panels = dashboard.state.body.getVizPanels(); + const panel = panels[0]; - expect(layout.state.children.length).toBe(1); - expect(gridItem.state.body!).toBeInstanceOf(VizPanel); - expect(gridItem.state.body.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior); - expect(gridItem.state.body.state.key).toBe('panel-1'); + expect(panels.length).toBe(1); + expect(panel.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior); + expect(panel.state.key).toBe('panel-1'); expect(dashboard.state.isEditing).toBe(true); }); - it('should throw error if adding lib panel in a layout that is not SceneGridLayout', () => { - dashboard.setState({ body: undefined }); - - expect(() => addLibPanelDrawer.onAddLibraryPanel({} as LibraryPanel)).toThrow( - 'Trying to add a library panel in a layout that is not SceneGridLayout' - ); - }); - it('should replace grid item when grid item state is passed', async () => { const libPanel = new VizPanel({ title: 'Panel Title', @@ -112,10 +99,6 @@ describe('AddLibraryPanelWidget', () => { $behaviors: [new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: 'uid' })], }); - let gridItem = new DashboardGridItem({ - body: libPanel, - key: 'grid-item-1', - }); addLibPanelDrawer = new AddLibraryPanelDrawer({ panelToReplaceRef: libPanel.getRef() }); dashboard = new DashboardScene({ $timeRange: new SceneTimeRange({}), @@ -125,9 +108,7 @@ describe('AddLibraryPanelWidget', () => { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [gridItem], - }), + body: DefaultGridLayoutManager.fromVizPanels([libPanel]), overlay: addLibPanelDrawer, }); @@ -143,71 +124,12 @@ describe('AddLibraryPanelWidget', () => { addLibPanelDrawer.onAddLibraryPanel(panelInfo); - const layout = dashboard.state.body as SceneGridLayout; - gridItem = layout.state.children[0] as DashboardGridItem; - const behavior = gridItem.state.body!.state.$behaviors![0] as LibraryPanelBehavior; - - expect(layout.state.children.length).toBe(1); - expect(gridItem.state.body!).toBeInstanceOf(VizPanel); - expect(behavior).toBeInstanceOf(LibraryPanelBehavior); - expect(gridItem.state.key).toBe('grid-item-1'); - expect(behavior.state.uid).toBe('new_uid'); - expect(behavior.state.name).toBe('new_name'); - }); - - it('should replace grid item in row when grid item state is passed', async () => { - const libPanel = new VizPanel({ - title: 'Panel Title', - pluginId: 'table', - key: 'panel-1', - $behaviors: [new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: 'uid' })], - }); - - let gridItem = new DashboardGridItem({ - body: libPanel, - key: 'grid-item-1', - }); - addLibPanelDrawer = new AddLibraryPanelDrawer({ panelToReplaceRef: libPanel.getRef() }); - dashboard = new DashboardScene({ - $timeRange: new SceneTimeRange({}), - title: 'hello', - uid: 'dash-1', - version: 4, - meta: { - canEdit: true, - }, - body: new SceneGridLayout({ - children: [ - new SceneGridRow({ - children: [gridItem], - }), - ], - }), - overlay: addLibPanelDrawer, - }); - - const panelInfo: LibraryPanel = { - uid: 'new_uid', - model: { - type: 'timeseries', - }, - name: 'new_name', - version: 1, - type: 'timeseries', - }; - - addLibPanelDrawer.onAddLibraryPanel(panelInfo); + const panels = dashboard.state.body.getVizPanels(); + expect(panels.length).toBe(1); - const layout = dashboard.state.body as SceneGridLayout; - const gridRow = layout.state.children[0] as SceneGridRow; - gridItem = gridRow.state.children[0] as DashboardGridItem; - const behavior = gridItem.state.body!.state.$behaviors![0] as LibraryPanelBehavior; + const behavior = panels[0].state.$behaviors![0] as LibraryPanelBehavior; - expect(layout.state.children.length).toBe(1); - expect(gridRow.state.children.length).toBe(1); - expect(gridItem.state.body!).toBeInstanceOf(VizPanel); expect(behavior).toBeInstanceOf(LibraryPanelBehavior); - expect(gridItem.state.key).toBe('grid-item-1'); expect(behavior.state.uid).toBe('new_uid'); expect(behavior.state.name).toBe('new_name'); }); @@ -223,9 +145,6 @@ async function buildTestScene() { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [], - }), overlay: drawer, }); diff --git a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx index 54f7679c87a..a06154459ff 100644 --- a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx +++ b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx @@ -1,11 +1,4 @@ -import { - SceneComponentProps, - SceneGridLayout, - SceneObjectBase, - SceneObjectRef, - SceneObjectState, - VizPanel, -} from '@grafana/scenes'; +import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState, VizPanel } from '@grafana/scenes'; import { LibraryPanel } from '@grafana/schema'; import { Drawer } from '@grafana/ui'; import { t } from 'app/core/internationalization'; @@ -14,8 +7,7 @@ import { LibraryPanelsSearchVariant, } from 'app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch'; -import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; -import { NEW_PANEL_HEIGHT, NEW_PANEL_WIDTH, getDashboardSceneFor, getDefaultVizPanel } from '../utils/utils'; +import { getDashboardSceneFor, getDefaultVizPanel } from '../utils/utils'; import { DashboardGridItem } from './DashboardGridItem'; import { LibraryPanelBehavior } from './LibraryPanelBehavior'; @@ -31,16 +23,10 @@ export class AddLibraryPanelDrawer extends SceneObjectBase { const dashboard = getDashboardSceneFor(this); - const layout = dashboard.state.body; - - if (!(layout instanceof SceneGridLayout)) { - throw new Error('Trying to add a library panel in a layout that is not SceneGridLayout'); - } - const panelId = dashboardSceneGraph.getNextPanelId(dashboard); + const newPanel = getDefaultVizPanel(dashboard); - const body = getDefaultVizPanel(dashboard); - body.setState({ + newPanel.setState({ $behaviors: [new LibraryPanelBehavior({ uid: panelInfo.uid, name: panelInfo.name })], }); @@ -53,22 +39,9 @@ export class AddLibraryPanelDrawer extends SceneObjectBase { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: sourcePanel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([sourcePanel]), }); activateFullSceneTree(scene); @@ -183,22 +172,10 @@ describe('DashboardDatasourceBehaviour', () => { const spy = jest.spyOn(dashboardDSPanel.state.$data as SceneQueryRunner, 'runQueries'); - const layout = scene.state.body as SceneGridLayout; + //const layout = scene.state.body as DefaultGridLayoutManager; // we add the new panel, it should run it's query as usual - layout.setState({ - children: [ - ...layout.state.children, - new DashboardGridItem({ - key: 'griditem-2', - x: 0, - y: 0, - width: 10, - height: 12, - body: dashboardDSPanel, - }), - ], - }); + scene.addPanel(dashboardDSPanel); dashboardDSPanel.activate(); @@ -237,26 +214,7 @@ describe('DashboardDatasourceBehaviour', () => { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: sourcePanel, - }), - new DashboardGridItem({ - key: 'griditem-2', - x: 0, - y: 0, - width: 10, - height: 12, - body: dashboardDSPanel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]), }); const spy = jest.spyOn(dashboardDSPanel.state.$data as SceneQueryRunner, 'runQueries'); @@ -313,26 +271,7 @@ describe('DashboardDatasourceBehaviour', () => { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: sourcePanel, - }), - new DashboardGridItem({ - key: 'griditem-2', - x: 0, - y: 0, - width: 10, - height: 12, - body: dashboardDSPanel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]), }); const sceneDeactivate = activateFullSceneTree(scene); @@ -382,26 +321,7 @@ describe('DashboardDatasourceBehaviour', () => { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: sourcePanel, - }), - new DashboardGridItem({ - key: 'griditem-2', - x: 0, - y: 0, - width: 10, - height: 12, - body: anotherPanel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, anotherPanel]), }); const sceneDeactivate = activateFullSceneTree(scene); @@ -457,26 +377,7 @@ describe('DashboardDatasourceBehaviour', () => { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: sourcePanel, - }), - new DashboardGridItem({ - key: 'griditem-2', - x: 0, - y: 0, - width: 10, - height: 12, - body: dashboardDSPanel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]), }); try { @@ -526,26 +427,7 @@ describe('DashboardDatasourceBehaviour', () => { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: sourcePanel, - }), - new DashboardGridItem({ - key: 'griditem-2', - x: 0, - y: 0, - width: 10, - height: 12, - body: dashboardDSPanel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]), }); const sceneDeactivate = activateFullSceneTree(scene); @@ -605,26 +487,7 @@ describe('DashboardDatasourceBehaviour', () => { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: sourcePanel, - }), - new DashboardGridItem({ - key: 'griditem-2', - x: 0, - y: 0, - width: 10, - height: 12, - body: dashboardDSPanel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]), }); activateFullSceneTree(scene); @@ -684,26 +547,7 @@ async function buildTestScene() { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: sourcePanel, - }), - new DashboardGridItem({ - key: 'griditem-2', - x: 0, - y: 0, - width: 10, - height: 12, - body: dashboardDSPanel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]), }); const sceneDeactivate = activateFullSceneTree(scene); diff --git a/public/app/features/dashboard-scene/scene/DashboardGridItem.test.tsx b/public/app/features/dashboard-scene/scene/DashboardGridItem.test.tsx index de74e9934a7..cb695ff747d 100644 --- a/public/app/features/dashboard-scene/scene/DashboardGridItem.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardGridItem.test.tsx @@ -8,6 +8,7 @@ import { activateFullSceneTree, buildPanelRepeaterScene } from '../utils/test-ut import { DashboardGridItem, DashboardGridItemState } from './DashboardGridItem'; import { DashboardScene } from './DashboardScene'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), @@ -122,8 +123,8 @@ describe('PanelRepeaterGridItem', () => { $variables: new SceneVariableSet({ variables: [variable], }), - body: new SceneGridLayout({ - children: [panel], + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ children: [panel] }), }), }); @@ -193,8 +194,10 @@ describe('PanelRepeaterGridItem', () => { $variables: new SceneVariableSet({ variables: [variable], }), - body: new SceneGridLayout({ - children: [panel, panel2], + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [panel, panel2], + }), }), }); @@ -269,7 +272,8 @@ describe('PanelRepeaterGridItem', () => { const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 0, maxPerRow: 2, itemHeight: 10 }); const layoutForceRender = jest.fn(); - (scene.state.body as SceneGridLayout).forceRender = layoutForceRender; + const layout = scene.state.body as DefaultGridLayoutManager; + layout.state.grid.forceRender = layoutForceRender; activateFullSceneTree(scene); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 3df3a8f73e6..6dd0aedbfdf 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -25,13 +25,14 @@ import { DecoratedRevisionModel } from '../settings/VersionsEditView'; import { historySrv } from '../settings/version-history/HistorySrv'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { djb2Hash } from '../utils/djb2Hash'; -import { findVizPanelByKey } from '../utils/utils'; +import { findVizPanelByKey, getLibraryPanelBehavior, isLibraryPanel } from '../utils/utils'; import { DashboardControls } from './DashboardControls'; import { DashboardGridItem } from './DashboardGridItem'; import { DashboardScene, DashboardSceneState } from './DashboardScene'; import { LibraryPanelBehavior } from './LibraryPanelBehavior'; import { PanelTimeRange } from './PanelTimeRange'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; import { RowActions } from './row-actions/RowActions'; jest.mock('../settings/version-history/HistorySrv'); @@ -343,9 +344,8 @@ describe('DashboardScene', () => { }); it('A change to any library panel name should set isDirty true', () => { - const libraryVizPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem).state - .body; - const behavior = libraryVizPanel.state.$behaviors![0] as LibraryPanelBehavior; + const panel = findVizPanelByKey(scene, 'panel-5')!; + const behavior = getLibraryPanelBehavior(panel)!; const prevValue = behavior.state.name; behavior.setState({ name: 'new name' }); @@ -353,9 +353,9 @@ describe('DashboardScene', () => { expect(scene.state.isDirty).toBe(true); scene.exitEditMode({ skipConfirm: true }); - const restoredLibraryVizPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem) - .state.body; - const restoredBehavior = restoredLibraryVizPanel.state.$behaviors![0] as LibraryPanelBehavior; + + const restoredPanel = findVizPanelByKey(scene, 'panel-5')!; + const restoredBehavior = getLibraryPanelBehavior(restoredPanel)!; expect(restoredBehavior.state.name).toBe(prevValue); }); @@ -451,160 +451,15 @@ describe('DashboardScene', () => { expect(scene.state.isDirty).toBeFalsy(); }); - it('Should throw an error when adding a panel to a layout that is not SceneGridLayout', () => { - const scene = buildTestScene({ body: undefined }); - - expect(() => { - scene.addPanel(new VizPanel({ title: 'Panel Title', key: 'panel-4', pluginId: 'timeseries' })); - }).toThrow('Trying to add a panel in a layout that is not SceneGridLayout'); - }); - - it('Should add a new panel to the dashboard', () => { - const vizPanel = new VizPanel({ - title: 'Panel Title', - key: 'panel-5', - pluginId: 'timeseries', - $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), - }); - - scene.addPanel(vizPanel); - - const body = scene.state.body as SceneGridLayout; - const gridItem = body.state.children[0] as DashboardGridItem; - - expect(body.state.children.length).toBe(6); - expect(gridItem.state.body!.state.key).toBe('panel-5'); - expect(gridItem.state.y).toBe(0); - }); - it('Should create and add a new panel to the dashboard', () => { scene.exitEditMode({ skipConfirm: true }); expect(scene.state.isEditing).toBe(false); - scene.onCreateNewPanel(); - - const body = scene.state.body as SceneGridLayout; - const gridItem = body.state.children[0] as DashboardGridItem; + const panel = scene.onCreateNewPanel(); expect(scene.state.isEditing).toBe(true); - expect(body.state.children.length).toBe(6); - expect(gridItem.state.body!.state.key).toBe('panel-7'); - }); - - it('Should create and add a new row to the dashboard', () => { - scene.onCreateNewRow(); - - const body = scene.state.body as SceneGridLayout; - const gridRow = body.state.children[0] as SceneGridRow; - - expect(scene.state.isEditing).toBe(true); - expect(body.state.children.length).toBe(4); - expect(gridRow.state.key).toBe('panel-7'); - expect(gridRow.state.children[0].state.key).toBe('griditem-1'); - expect(gridRow.state.children[1].state.key).toBe('griditem-2'); - }); - - it('Should create a row and add all panels in the dashboard under it', () => { - const scene = buildTestScene({ - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), - }), - }), - new DashboardGridItem({ - key: 'griditem-2', - body: new VizPanel({ - title: 'Panel B', - key: 'panel-2', - pluginId: 'table', - }), - }), - ], - }), - }); - - scene.onCreateNewRow(); - - const body = scene.state.body as SceneGridLayout; - const gridRow = body.state.children[0] as SceneGridRow; - - expect(body.state.children.length).toBe(1); - expect(gridRow.state.children.length).toBe(2); - }); - - it('Should create and add two new rows, but the second has no children', () => { - scene.onCreateNewRow(); - scene.onCreateNewRow(); - - const body = scene.state.body as SceneGridLayout; - const gridRow = body.state.children[0] as SceneGridRow; - - expect(body.state.children.length).toBe(5); - expect(gridRow.state.children.length).toBe(0); - }); - - it('Should create an empty row when nothing else in dashboard', () => { - const scene = buildTestScene({ - body: new SceneGridLayout({ - children: [], - }), - }); - - scene.onCreateNewRow(); - - const body = scene.state.body as SceneGridLayout; - const gridRow = body.state.children[0] as SceneGridRow; - - expect(body.state.children.length).toBe(1); - expect(gridRow.state.children.length).toBe(0); - }); - - it('Should remove a row and move its children to the grid layout', () => { - const body = scene.state.body as SceneGridLayout; - const row = body.state.children[2] as SceneGridRow; - - scene.removeRow(row); - - const vizPanel = (body.state.children[2] as DashboardGridItem).state.body as VizPanel; - - expect(body.state.children.length).toBe(6); - expect(vizPanel.state.key).toBe('panel-4'); - }); - - it('Should remove a row and its children', () => { - const body = scene.state.body as SceneGridLayout; - const row = body.state.children[2] as SceneGridRow; - - scene.removeRow(row, true); - - expect(body.state.children.length).toBe(4); - }); - - it('Should remove an empty row from the layout', () => { - const row = new SceneGridRow({ - key: 'panel-1', - }); - - const scene = buildTestScene({ - body: new SceneGridLayout({ - children: [row], - }), - }); - - const body = scene.state.body as SceneGridLayout; - - expect(body.state.children.length).toBe(1); - - scene.removeRow(row); - - expect(body.state.children.length).toBe(0); + expect(scene.state.body.getVizPanels().length).toBe(7); + expect(panel.state.key).toBe('panel-7'); }); it('Should fail to copy a panel if it does not have a grid item parent', () => { @@ -633,16 +488,16 @@ describe('DashboardScene', () => { }); it('Should copy a panel', () => { - const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state.body; + const vizPanel = findVizPanelByKey(scene, 'panel-1')!; scene.copyPanel(vizPanel as VizPanel); expect(store.exists(LS_PANEL_COPY_KEY)).toBe(true); }); it('Should copy a library viz panel', () => { - const libVizPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem).state.body; + const libVizPanel = findVizPanelByKey(scene, 'panel-6')!; - expect(libVizPanel.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior); + expect(isLibraryPanel(libVizPanel)).toBe(true); scene.copyPanel(libVizPanel); @@ -651,7 +506,6 @@ describe('DashboardScene', () => { it('Should paste a panel', () => { store.set(LS_PANEL_COPY_KEY, JSON.stringify({ key: 'panel-7' })); - jest.spyOn(JSON, 'parse').mockReturnThis(); jest.mocked(buildGridItemForPanel).mockReturnValue( new DashboardGridItem({ key: 'griditem-9', @@ -665,19 +519,15 @@ describe('DashboardScene', () => { scene.pastePanel(); - const body = scene.state.body as SceneGridLayout; - const gridItem = body.state.children[0] as DashboardGridItem; - expect(buildGridItemForPanel).toHaveBeenCalledTimes(1); - expect(body.state.children.length).toBe(6); - expect(gridItem.state.body!.state.key).toBe('panel-7'); - expect(gridItem.state.y).toBe(0); + + const addedPanel = findVizPanelByKey(scene, 'panel-7')!; + expect(addedPanel).toBeDefined(); expect(store.exists(LS_PANEL_COPY_KEY)).toBe(false); }); it('Should paste a library viz panel', () => { store.set(LS_PANEL_COPY_KEY, JSON.stringify({ key: 'panel-7' })); - jest.spyOn(JSON, 'parse').mockReturnValue({ libraryPanel: { uid: 'uid', name: 'libraryPanel' } }); jest.mocked(buildGridItemForPanel).mockReturnValue( new DashboardGridItem({ body: new VizPanel({ @@ -691,198 +541,12 @@ describe('DashboardScene', () => { scene.pastePanel(); - const body = scene.state.body as SceneGridLayout; - const gridItem = body.state.children[0] as DashboardGridItem; - - const libVizPanel = gridItem.state.body; - expect(buildGridItemForPanel).toHaveBeenCalledTimes(1); - expect(body.state.children.length).toBe(6); - expect(libVizPanel.state.key).toBe('panel-7'); - expect(gridItem.state.y).toBe(0); - expect(store.exists(LS_PANEL_COPY_KEY)).toBe(false); - }); - - it('Should remove a panel', () => { - const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state.body; - scene.removePanel(vizPanel as VizPanel); - - const body = scene.state.body as SceneGridLayout; - expect(body.state.children.length).toBe(4); - }); - - it('Should remove a panel within a row', () => { - const vizPanel = ( - ((scene.state.body as SceneGridLayout).state.children[2] as SceneGridRow).state - .children[0] as DashboardGridItem - ).state.body; - scene.removePanel(vizPanel as VizPanel); - - const body = scene.state.body as SceneGridLayout; - const gridRow = body.state.children[2] as SceneGridRow; - - expect(gridRow.state.children.length).toBe(1); - }); - - it('Should remove a library panel', () => { - const libraryPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem).state.body; - scene.removePanel(libraryPanel); - - const body = scene.state.body as SceneGridLayout; - expect(body.state.children.length).toBe(4); - }); - - it('Should remove a library panel within a row', () => { - const libraryPanel = ( - ((scene.state.body as SceneGridLayout).state.children[2] as SceneGridRow).state - .children[1] as DashboardGridItem - ).state.body; - scene.removePanel(libraryPanel); - - const body = scene.state.body as SceneGridLayout; - const gridRow = body.state.children[2] as SceneGridRow; - expect(gridRow.state.children.length).toBe(1); - }); - - it('Should duplicate a panel', () => { - const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state.body; - scene.duplicatePanel(vizPanel as VizPanel); - - const body = scene.state.body as SceneGridLayout; - const gridItem = body.state.children[5] as DashboardGridItem; - - expect(body.state.children.length).toBe(6); - expect(gridItem.state.body!.state.key).toBe('panel-7'); - }); - - it('Should maintain size of duplicated panel', () => { - const gItem = (scene.state.body as SceneGridLayout).state.children[0] as DashboardGridItem; - gItem.setState({ height: 1 }); - const vizPanel = gItem.state.body; - scene.duplicatePanel(vizPanel as VizPanel); - - const body = scene.state.body as SceneGridLayout; - const newGridItem = body.state.children[5] as DashboardGridItem; - - expect(body.state.children.length).toBe(6); - expect(newGridItem.state.body!.state.key).toBe('panel-7'); - expect(newGridItem.state.height).toBe(1); - }); - - it('Should duplicate a library panel', () => { - const libraryPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem).state.body; - scene.duplicatePanel(libraryPanel); - - const body = scene.state.body as SceneGridLayout; - const gridItem = body.state.children[5] as DashboardGridItem; - - const libVizPanel = gridItem.state.body; - - expect(body.state.children.length).toBe(6); - expect(libVizPanel.state.key).toBe('panel-7'); - }); - - it('Should deep clone data provider when duplicating a panel', () => { - const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state.body; - scene.duplicatePanel(vizPanel as VizPanel); - - const panelQueries = ( - ((scene.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state.body.state.$data?.state - .$data as SceneQueryRunner - ).state.queries; - const duplicatedPanelQueries = ( - ((scene.state.body as SceneGridLayout).state.children[5] as DashboardGridItem).state.body.state.$data?.state - .$data as SceneQueryRunner - ).state.queries; - - expect(panelQueries[0]).not.toBe(duplicatedPanelQueries[0]); - }); - - it('Should duplicate a repeated panel', () => { - const scene = buildTestScene({ - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: `grid-item-1`, - width: 24, - height: 8, - repeatedPanels: [ - new VizPanel({ - title: 'Library Panel', - key: 'panel-1', - pluginId: 'table', - }), - ], - body: new VizPanel({ - title: 'Library Panel', - key: 'panel-1', - pluginId: 'table', - }), - variableName: 'custom', - }), - ], - }), - }); - - const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state - .repeatedPanels![0]; - - scene.duplicatePanel(vizPanel as VizPanel); - - const body = scene.state.body as SceneGridLayout; - const gridItem = body.state.children[1] as DashboardGridItem; - - 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 DashboardGridItem - ).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 DashboardGridItem; - - 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 DashboardGridItem - ).state.body; - - scene.duplicatePanel(libraryPanel); - - const body = scene.state.body as SceneGridLayout; - const gridRow = body.state.children[2] as SceneGridRow; - const gridItem = gridRow.state.children[2] as DashboardGridItem; - - const libVizPanel = gridItem.state.body; - - expect(gridRow.state.children.length).toBe(3); - expect(libVizPanel.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); + const addedPanel = findVizPanelByKey(scene, 'panel-7')!; + expect(addedPanel).toBeDefined(); + expect(addedPanel.state.key).toBe('panel-7'); + expect(store.exists(LS_PANEL_COPY_KEY)).toBe(false); }); it('Should unlink a library panel', () => { @@ -893,24 +557,14 @@ describe('DashboardScene', () => { }); const scene = buildTestScene({ - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-2', - body: libPanel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([libPanel]), }); - scene.unlinkLibraryPanel(libPanel); + expect(isLibraryPanel(libPanel)).toBe(true); - const body = scene.state.body as SceneGridLayout; - const gridItem = body.state.children[0] as DashboardGridItem; + scene.unlinkLibraryPanel(libPanel); - expect(body.state.children.length).toBe(1); - expect(gridItem.state.body).toBeInstanceOf(VizPanel); - expect(gridItem.state.$behaviors).toBeUndefined(); + expect(isLibraryPanel(libPanel)).toBe(false); }); it('Should create a library panel', () => { @@ -925,50 +579,9 @@ describe('DashboardScene', () => { body: vizPanel, }); + const grid = new SceneGridLayout({ children: [gridItem] }); const scene = buildTestScene({ - body: new SceneGridLayout({ - children: [gridItem], - }), - }); - - const libPanel = { - uid: 'uid', - name: 'name', - }; - - scene.createLibraryPanel(vizPanel, libPanel as LibraryPanel); - - const layout = scene.state.body as SceneGridLayout; - const newGridItem = layout.state.children[0] as DashboardGridItem; - const behavior = newGridItem.state.body.state.$behaviors![0] as LibraryPanelBehavior; - - expect(layout.state.children.length).toBe(1); - expect(newGridItem.state.body).toBeInstanceOf(VizPanel); - expect(behavior.state.uid).toBe('uid'); - expect(behavior.state.name).toBe('name'); - }); - - it('Should create a library panel under a row', () => { - const vizPanel = new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - }); - - const gridItem = new DashboardGridItem({ - key: 'griditem-1', - body: vizPanel, - }); - - const scene = buildTestScene({ - body: new SceneGridLayout({ - children: [ - new SceneGridRow({ - key: 'row-1', - children: [gridItem], - }), - ], - }), + body: new DefaultGridLayoutManager({ grid }), }); const libPanel = { @@ -978,12 +591,10 @@ describe('DashboardScene', () => { scene.createLibraryPanel(vizPanel, libPanel as LibraryPanel); - const layout = scene.state.body as SceneGridLayout; - const newGridItem = (layout.state.children[0] as SceneGridRow).state.children[0] as DashboardGridItem; + const newGridItem = grid.state.children[0] as DashboardGridItem; const behavior = newGridItem.state.body.state.$behaviors![0] as LibraryPanelBehavior; - expect(layout.state.children.length).toBe(1); - expect((layout.state.children[0] as SceneGridRow).state.children.length).toBe(1); + expect(grid.state.children.length).toBe(1); expect(newGridItem.state.body).toBeInstanceOf(VizPanel); expect(behavior.state.uid).toBe('uid'); expect(behavior.state.name).toBe('name'); @@ -1166,6 +777,19 @@ describe('DashboardScene', () => { describe('When coming from explore', () => { // When coming from Explore the first panel in a dashboard is a temporary panel it('should remove first panel from the grid when discarding changes', () => { + const layout = DefaultGridLayoutManager.fromVizPanels([ + new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }), + new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', + }), + ]); const scene = new DashboardScene({ title: 'hello', uid: 'dash-1', @@ -1176,37 +800,18 @@ describe('DashboardScene', () => { }), controls: new DashboardControls({}), $behaviors: [new behaviors.CursorSync({})], - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), - }), - }), - new DashboardGridItem({ - key: 'griditem-2', - body: new VizPanel({ - title: 'Panel B', - key: 'panel-2', - pluginId: 'table', - }), - }), - ], - }), + body: layout, }); scene.onEnterEditMode(true); expect(scene.state.isEditing).toBe(true); - expect((scene.state.body as SceneGridLayout).state.children.length).toBe(2); + expect(layout.state.grid.state.children.length).toBe(2); scene.exitEditMode({ skipConfirm: true }); + + const restoredGrid = scene.state.body as DefaultGridLayoutManager; expect(scene.state.isEditing).toBe(false); - expect((scene.state.body as SceneGridLayout).state.children.length).toBe(1); + expect(restoredGrid.state.grid.state.children.length).toBe(1); }); }); }); @@ -1223,75 +828,74 @@ function buildTestScene(overrides?: Partial) { }), controls: new DashboardControls({}), $behaviors: [new behaviors.CursorSync({}), new behaviors.LiveNowTimer({})], - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - $timeRange: new PanelTimeRange({ - from: 'now-12h', - to: 'now', - timeZone: 'browser', - }), - $data: new SceneDataTransformer({ - transformations: [], - $data: new SceneQueryRunner({ - key: 'data-query-runner', - queries: [{ refId: 'A', target: 'aliasByMetric(carbon.**)' }], + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [ + new DashboardGridItem({ + key: 'griditem-1', + x: 0, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $timeRange: new PanelTimeRange({ + from: 'now-12h', + to: 'now', + timeZone: 'browser', + }), + $data: new SceneDataTransformer({ + transformations: [], + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), }), }), }), - }), - new DashboardGridItem({ - key: 'griditem-2', - body: new VizPanel({ - title: 'Panel B', - key: 'panel-2', - pluginId: 'table', + new DashboardGridItem({ + key: 'griditem-2', + body: new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', + }), }), - }), - new SceneGridRow({ - key: 'panel-3', - actions: new RowActions({}), - children: [ - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel C', - key: 'panel-4', - pluginId: 'table', + new SceneGridRow({ + key: 'panel-3', + actions: new RowActions({}), + children: [ + new DashboardGridItem({ + body: new VizPanel({ + title: 'Panel C', + key: 'panel-4', + pluginId: 'table', + }), }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Library Panel', - pluginId: 'table', - key: 'panel-5', - $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + new DashboardGridItem({ + body: new VizPanel({ + title: 'Library Panel', + pluginId: 'table', + key: 'panel-5', + $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + }), }), + ], + }), + new DashboardGridItem({ + body: new VizPanel({ + title: 'Panel B', + key: 'panel-2-clone-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner2', queries: [{ refId: 'A' }] }), }), - ], - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel B', - key: 'panel-2-clone-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner2', queries: [{ refId: 'A' }] }), }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Library Panel', - pluginId: 'table', - key: 'panel-6', - $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + new DashboardGridItem({ + body: new VizPanel({ + title: 'Library Panel', + pluginId: 'table', + key: 'panel-6', + $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], + }), }), - }), - ], + ], + }), }), ...overrides, }); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 6c1bffe482a..72c7025a25f 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -12,14 +12,12 @@ import { } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { - SceneFlexLayout, - sceneGraph, - SceneGridLayout, SceneGridRow, SceneObject, SceneObjectBase, SceneObjectRef, SceneObjectState, + SceneTimeRange, sceneUtils, SceneVariable, SceneVariableDependencyConfigLike, @@ -52,15 +50,10 @@ import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { djb2Hash } from '../utils/djb2Hash'; import { getDashboardUrl, getViewPanelUrl } from '../utils/urlBuilders'; import { - NEW_PANEL_HEIGHT, - NEW_PANEL_WIDTH, - forceRenderChildren, getClosestVizPanel, getDashboardSceneFor, - getDefaultRow, getDefaultVizPanel, getPanelIdForVizPanel, - getQueryRunnerFor, getVizPanelKeyForPanelId, isPanelClone, } from '../utils/utils'; @@ -74,6 +67,8 @@ import { LibraryPanelBehavior } from './LibraryPanelBehavior'; import { RowRepeaterBehavior } from './RowRepeaterBehavior'; import { ViewPanelScene } from './ViewPanelScene'; import { setupKeyboardShortcuts } from './keyboardShortcuts'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; +import { DashboardLayoutManager } from './types'; export const PERSISTED_PROPS = ['title', 'description', 'tags', 'editable', 'graphTooltip', 'links', 'meta', 'preload']; @@ -95,7 +90,7 @@ export interface DashboardSceneState extends SceneObjectState { /** @deprecated */ id?: number | null; /** Layout of panels */ - body: SceneObject; + body: DashboardLayoutManager; /** NavToolbar actions */ actions?: SceneObject[]; /** Fixed row at the top of the canvas with for example variables and time range controls */ @@ -175,7 +170,8 @@ export class DashboardScene extends SceneObjectBase { title: 'Dashboard', meta: {}, editable: true, - body: state.body ?? new SceneFlexLayout({ children: [] }), + $timeRange: state.$timeRange ?? new SceneTimeRange({}), + body: state.body ?? DefaultGridLayoutManager.fromVizPanels(), links: state.links ?? [], ...state, }); @@ -235,7 +231,7 @@ export class DashboardScene extends SceneObjectBase { this.setState({ isEditing: true }); // Propagate change edit mode change to children - this.propagateEditModeChange(); + this.state.body.editModeChanged(true); // Propagate edit mode to scopes this._scopesFacade?.enterReadOnly(); @@ -272,13 +268,6 @@ export class DashboardScene extends SceneObjectBase { this._changeTracker.startTrackingChanges(); } - private propagateEditModeChange() { - if (this.state.body instanceof SceneGridLayout) { - this.state.body.setState({ isDraggable: this.state.isEditing, isResizable: this.state.isEditing }); - forceRenderChildren(this.state.body, true); - } - } - public exitEditMode({ skipConfirm, restoreInitialState }: { skipConfirm: boolean; restoreInitialState?: boolean }) { if (!this.canDiscard()) { console.error('Trying to discard back to a state that does not exist, initialState undefined'); @@ -342,7 +331,7 @@ export class DashboardScene extends SceneObjectBase { } // Disable grid dragging - this.propagateEditModeChange(); + this.state.body.editModeChanged(false); } private cleanupStateFromExplore() { @@ -352,10 +341,8 @@ export class DashboardScene extends SceneObjectBase { this._initialSaveModel.panels = this._initialSaveModel.panels.slice(1); } - if (this._initialState && this._initialState.body instanceof SceneGridLayout) { - this._initialState.body.setState({ - children: this._initialState.body.state.children.slice(1), - }); + if (this._initialState) { + this._initialState.body.cleanUpStateFromExplore?.(); } } @@ -467,79 +454,15 @@ export class DashboardScene extends SceneObjectBase { return this._initialState; } - public addRow(row: SceneGridRow) { - if (!(this.state.body instanceof SceneGridLayout)) { - throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); - } - - const sceneGridLayout = this.state.body; - - // find all panels until the first row and put them into the newly created row. If there are no other rows, - // add all panels to the row. If there are no panels just create an empty row - const indexTillNextRow = sceneGridLayout.state.children.findIndex((child) => child instanceof SceneGridRow); - const rowChildren = sceneGridLayout.state.children - .splice(0, indexTillNextRow === -1 ? sceneGridLayout.state.children.length : indexTillNextRow) - .map((child) => child.clone()); - - if (rowChildren) { - row.setState({ - children: rowChildren, - }); - } - - sceneGridLayout.setState({ - children: [row, ...sceneGridLayout.state.children], - }); - } - - public removeRow(row: SceneGridRow, removePanels = false) { - if (!(this.state.body instanceof SceneGridLayout)) { - throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); - } - - const sceneGridLayout = this.state.body; - - const children = sceneGridLayout.state.children.filter((child) => child.state.key !== row.state.key); - - if (!removePanels) { - const rowChildren = row.state.children.map((child) => child.clone()); - const indexOfRow = sceneGridLayout.state.children.findIndex((child) => child.state.key === row.state.key); - - children.splice(indexOfRow, 0, ...rowChildren); - } - - sceneGridLayout.setState({ children }); - } - public addPanel(vizPanel: VizPanel): void { - if (!(this.state.body instanceof SceneGridLayout)) { - throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); + if (!this.state.isEditing) { + this.onEnterEditMode(); } - const sceneGridLayout = this.state.body; - - const panelId = getPanelIdForVizPanel(vizPanel); - const newGridItem = new DashboardGridItem({ - height: NEW_PANEL_HEIGHT, - width: NEW_PANEL_WIDTH, - x: 0, - y: 0, - body: vizPanel, - key: `grid-item-${panelId}`, - }); - - sceneGridLayout.setState({ - children: [newGridItem, ...sceneGridLayout.state.children], - }); + this.state.body.addPanel(vizPanel); } public createLibraryPanel(panelToReplace: VizPanel, libPanel: LibraryPanel) { - const layout = this.state.body; - - if (!(layout instanceof SceneGridLayout)) { - throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); - } - const body = panelToReplace.clone({ $behaviors: [new LibraryPanelBehavior({ uid: libPanel.uid, name: libPanel.name })], }); @@ -554,74 +477,7 @@ export class DashboardScene extends SceneObjectBase { } public duplicatePanel(vizPanel: VizPanel) { - if (!vizPanel.parent) { - return; - } - - const gridItem = vizPanel.parent; - - if (!(gridItem instanceof DashboardGridItem)) { - console.error('Trying to duplicate a panel in a layout that is not DashboardGridItem'); - return; - } - - let panelState; - let panelData; - let newGridItem; - const newPanelId = dashboardSceneGraph.getNextPanelId(this); - - if (gridItem instanceof DashboardGridItem) { - panelState = sceneUtils.cloneSceneObjectState(gridItem.state.body.state); - - let queryRunner = getQueryRunnerFor(gridItem.state.body); - const queries = queryRunner?.state.queries.map((q) => ({ ...q })); - queryRunner = queryRunner?.clone({ queries }); - panelData = sceneGraph.getData(gridItem.state.body).clone({ $data: queryRunner }); - } else { - panelState = sceneUtils.cloneSceneObjectState(vizPanel.state); - - let queryRunner = getQueryRunnerFor(vizPanel); - const queries = queryRunner?.state.queries.map((q) => ({ ...q })); - queryRunner = queryRunner?.clone({ queries }); - panelData = sceneGraph.getData(vizPanel).clone({ $data: queryRunner }); - } - - // when we duplicate a panel we don't want to clone the alert state - delete panelData.state.data?.alertState; - - newGridItem = new DashboardGridItem({ - x: gridItem.state.x, - y: gridItem.state.y, - height: gridItem.state.height, - width: gridItem.state.width, - variableName: gridItem.state.variableName, - repeatDirection: gridItem.state.repeatDirection, - maxPerRow: gridItem.state.maxPerRow, - body: new VizPanel({ ...panelState, $data: panelData, key: getVizPanelKeyForPanelId(newPanelId) }), - }); - - if (!(this.state.body instanceof SceneGridLayout)) { - console.error('Trying to duplicate a panel in a layout that is not SceneGridLayout '); - return; - } - - 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], - }); + this.state.body.duplicatePanel(vizPanel); } public copyPanel(vizPanel: VizPanel) { @@ -643,83 +499,23 @@ export class DashboardScene extends SceneObjectBase { } public pastePanel() { - if (!(this.state.body instanceof SceneGridLayout)) { - throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); - } - const jsonData = store.get(LS_PANEL_COPY_KEY); const jsonObj = JSON.parse(jsonData); const panelModel = new PanelModel(jsonObj); const gridItem = buildGridItemForPanel(panelModel); - - const sceneGridLayout = this.state.body; - - if (!(gridItem instanceof DashboardGridItem)) { - throw new Error('Cannot paste invalid grid item'); - } - const panelId = dashboardSceneGraph.getNextPanelId(this); + const panel = gridItem.state.body; - gridItem.state.body.setState({ - key: getVizPanelKeyForPanelId(panelId), - }); + panel.setState({ key: getVizPanelKeyForPanelId(panelId) }); + panel.clearParent(); - gridItem.setState({ - height: NEW_PANEL_HEIGHT, - width: NEW_PANEL_WIDTH, - x: 0, - y: 0, - key: `grid-item-${panelId}`, - }); - - sceneGridLayout.setState({ - children: [gridItem, ...sceneGridLayout.state.children], - }); + this.state.body.addPanel(panel); store.delete(LS_PANEL_COPY_KEY); } public removePanel(panel: VizPanel) { - const panels: SceneObject[] = []; - const key = panel.parent?.state.key; - - if (!key) { - return; - } - - let row: SceneGridRow | undefined; - - try { - row = sceneGraph.getAncestor(panel, SceneGridRow); - } catch { - row = undefined; - } - - if (row) { - row.state.children.forEach((child: SceneObject) => { - if (child.state.key !== key) { - panels.push(child); - } - }); - - row.setState({ children: panels }); - - this.state.body.forceRender(); - - return; - } - - this.state.body.forEachChild((child: SceneObject) => { - if (child.state.key !== key) { - panels.push(child); - } - }); - - const layout = this.state.body; - - if (layout instanceof SceneGridLayout || layout instanceof SceneFlexLayout) { - layout.setState({ children: panels }); - } + this.state.body.removePanel(panel); } public unlinkLibraryPanel(panel: VizPanel) { @@ -775,18 +571,10 @@ export class DashboardScene extends SceneObjectBase { } public onCreateNewRow() { - const row = getDefaultRow(this); - - this.addRow(row); - - return getPanelIdForVizPanel(row); + this.state.body.addNewRow(); } public onCreateNewPanel(): VizPanel { - if (!this.state.isEditing) { - this.onEnterEditMode(); - } - const vizPanel = getDefaultVizPanel(this); this.addPanel(vizPanel); @@ -852,40 +640,6 @@ export class DashboardScene extends SceneObjectBase { locationService.replace('/'); } - public collapseAllRows() { - if (!(this.state.body instanceof SceneGridLayout)) { - throw new Error('Dashboard scene layout is not SceneGridLayout'); - } - - const sceneGridLayout = this.state.body; - - sceneGridLayout.state.children.forEach((child) => { - if (!(child instanceof SceneGridRow)) { - return; - } - if (!child.state.isCollapsed) { - sceneGridLayout.toggleRow(child); - } - }); - } - - public expandAllRows() { - if (!(this.state.body instanceof SceneGridLayout)) { - throw new Error('Dashboard scene layout is not SceneGridLayout'); - } - - const sceneGridLayout = this.state.body; - - sceneGridLayout.state.children.forEach((child) => { - if (!(child instanceof SceneGridRow)) { - return; - } - if (child.state.isCollapsed) { - sceneGridLayout.toggleRow(child); - } - }); - } - public onSetScrollRef = (scrollElement: ScrollRefElement): void => { this._scrollRef = scrollElement; }; @@ -925,11 +679,11 @@ export class DashboardVariableDependency implements SceneVariableDependencyConfi * The first repeated row has the row repeater behavior but it also has a local SceneVariableSet with a local variable value */ const layout = this._dashboard.state.body; - if (!(layout instanceof SceneGridLayout)) { + if (!(layout instanceof DefaultGridLayoutManager)) { return; } - for (const child of layout.state.children) { + for (const child of layout.state.grid.state.children) { if (!(child instanceof SceneGridRow) || !child.state.$behaviors) { continue; } diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts index c1a7172044f..e676a90930e 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts @@ -1,10 +1,11 @@ import { AppEvents } from '@grafana/data'; -import { SceneGridLayout, SceneQueryRunner, VizPanel } from '@grafana/scenes'; +import { SceneQueryRunner, VizPanel } from '@grafana/scenes'; import appEvents from 'app/core/app_events'; import { KioskMode } from 'app/types'; import { DashboardGridItem } from './DashboardGridItem'; import { DashboardScene } from './DashboardScene'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; import { DashboardRepeatsProcessedEvent } from './types'; describe('DashboardSceneUrlSync', () => { @@ -30,14 +31,17 @@ describe('DashboardSceneUrlSync', () => { it('Should set UNSAFE_fitPanels when url has autofitpanels', () => { const scene = buildTestScene(); scene.urlSync?.updateFromUrl({ autofitpanels: '' }); - expect((scene.state.body as SceneGridLayout).state.UNSAFE_fitPanels).toBe(true); + const layout = scene.state.body as DefaultGridLayoutManager; + + expect(layout.state.grid.state.UNSAFE_fitPanels).toBe(true); }); it('Should get the autofitpanels from the scene state', () => { const scene = buildTestScene(); expect(scene.urlSync?.getUrlState().autofitpanels).toBeUndefined(); - (scene.state.body as SceneGridLayout).setState({ UNSAFE_fitPanels: true }); + const layout = scene.state.body as DefaultGridLayoutManager; + layout.state.grid.setState({ UNSAFE_fitPanels: true }); expect(scene.urlSync?.getUrlState().autofitpanels).toBe('true'); }); @@ -89,8 +93,9 @@ describe('DashboardSceneUrlSync', () => { expect(errorNotice).toBe(0); // fake adding clone panel - const layout = scene.state.body as SceneGridLayout; - layout.setState({ + const layout = scene.state.body as DefaultGridLayoutManager; + + layout.state.grid.setState({ children: [ new DashboardGridItem({ key: 'griditem-1', @@ -114,27 +119,20 @@ function buildTestScene() { const scene = new DashboardScene({ title: 'hello', uid: 'dash-1', - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel B', - key: 'panel-2', - pluginId: 'table', - }), - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([ + new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }), + + new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', + }), + ]), }); return scene; diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts index 21335a1904d..2892c555d07 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts @@ -2,7 +2,7 @@ import { Unsubscribable } from 'rxjs'; import { AppEvents } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; -import { SceneGridLayout, SceneObjectUrlSyncHandler, SceneObjectUrlValues, VizPanel } from '@grafana/scenes'; +import { SceneObjectUrlSyncHandler, SceneObjectUrlValues, VizPanel } from '@grafana/scenes'; import appEvents from 'app/core/app_events'; import { KioskMode } from 'app/types'; @@ -16,6 +16,7 @@ import { findVizPanelByKey, getLibraryPanelBehavior, isPanelClone } from '../uti import { DashboardScene, DashboardSceneState } from './DashboardScene'; import { LibraryPanelBehavior } from './LibraryPanelBehavior'; import { ViewPanelScene } from './ViewPanelScene'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; import { DashboardRepeatsProcessedEvent } from './types'; export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { @@ -29,9 +30,10 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { getUrlState(): SceneObjectUrlValues { const state = this._scene.state; + return { inspect: state.inspectPanelKey, - autofitpanels: state.body instanceof SceneGridLayout && !!state.body.state.UNSAFE_fitPanels ? 'true' : undefined, + autofitpanels: this.getAutoFitPanels(), viewPanel: state.viewPanelScene?.getUrlKey(), editview: state.editview?.getUrlKey(), editPanel: state.editPanel?.getUrlKey() || undefined, @@ -40,6 +42,14 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { }; } + private getAutoFitPanels(): string | undefined { + if (this._scene.state.body instanceof DefaultGridLayoutManager) { + return this._scene.state.body.state.grid.state.UNSAFE_fitPanels ? 'true' : undefined; + } + + return undefined; + } + updateFromUrl(values: SceneObjectUrlValues): void { const { inspectPanelKey, viewPanelScene, isEditing, editPanel, shareView } = this._scene.state; const update: Partial = {}; @@ -142,11 +152,12 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { update.shareView = undefined; } - if (this._scene.state.body instanceof SceneGridLayout) { + const layout = this._scene.state.body; + if (layout instanceof DefaultGridLayoutManager) { const UNSAFE_fitPanels = typeof values.autofitpanels === 'string'; - if (!!this._scene.state.body.state.UNSAFE_fitPanels !== UNSAFE_fitPanels) { - this._scene.state.body.setState({ UNSAFE_fitPanels }); + if (!!layout.state.grid.state.UNSAFE_fitPanels !== UNSAFE_fitPanels) { + layout.state.grid.setState({ UNSAFE_fitPanels }); } } diff --git a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx index 7a558d8c5f5..78378863e89 100644 --- a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx @@ -13,6 +13,7 @@ import { activateFullSceneTree } from '../utils/test-utils'; import { DashboardGridItem } from './DashboardGridItem'; import { DashboardScene } from './DashboardScene'; import { LibraryPanelBehavior } from './LibraryPanelBehavior'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; setPluginImportUtils({ importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})), @@ -170,8 +171,10 @@ async function buildTestSceneWithLibraryPanel() { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [gridItem], + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [gridItem], + }), }), }); diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx index 39048ed5724..468bcc5adf9 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx @@ -5,15 +5,15 @@ import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; import { selectors } from '@grafana/e2e-selectors'; import { LocationServiceProvider, config, locationService } from '@grafana/runtime'; -import { SceneGridLayout, SceneQueryRunner, SceneTimeRange, UrlSyncContextProvider, VizPanel } from '@grafana/scenes'; +import { SceneQueryRunner, SceneTimeRange, UrlSyncContextProvider, VizPanel } from '@grafana/scenes'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; import { DashboardMeta } from 'app/types'; import { buildPanelEditScene } from '../panel-edit/PanelEditor'; -import { DashboardGridItem } from './DashboardGridItem'; import { DashboardScene } from './DashboardScene'; import { ToolbarActions } from './NavToolbarActions'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; jest.mock('app/features/playlist/PlaylistSrv', () => ({ playlistSrv: { @@ -120,9 +120,8 @@ describe('NavToolbarActions', () => { await act(() => { dashboard.onEnterEditMode(); - const editingPanel = ((dashboard.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state - .body as VizPanel; - dashboard.setState({ editPanel: buildPanelEditScene(editingPanel, true) }); + const panel = dashboard.state.body.getVizPanels()[0]; + dashboard.setState({ editPanel: buildPanelEditScene(panel, true) }); }); expect(await screen.findByText('Save dashboard')).toBeInTheDocument(); @@ -135,9 +134,8 @@ describe('NavToolbarActions', () => { await act(() => { dashboard.onEnterEditMode(); - const editingPanel = ((dashboard.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state - .body as VizPanel; - dashboard.setState({ editPanel: buildPanelEditScene(editingPanel) }); + const panel = dashboard.state.body.getVizPanels()[0]; + dashboard.setState({ editPanel: buildPanelEditScene(panel) }); }); expect(await screen.findByText('Save dashboard')).toBeInTheDocument(); @@ -207,27 +205,19 @@ function setup(meta?: DashboardMeta) { }, title: 'hello', uid: 'dash-1', - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel B', - key: 'panel-2', - pluginId: 'table', - }), - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([ + new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }), + new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', + }), + ]), }); const context = getGrafanaContextMock(); diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx index 5863a58fe47..5b0fa84910a 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx @@ -11,7 +11,6 @@ import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { getPluginLinkExtensions, locationService } from '@grafana/runtime'; import { LocalValueVariable, - SceneGridLayout, SceneQueryRunner, SceneTimeRange, SceneVariableSet, @@ -23,10 +22,10 @@ import { GetExploreUrlArguments } from 'app/core/utils/explore'; import { buildPanelEditScene } from '../panel-edit/PanelEditor'; -import { DashboardGridItem } from './DashboardGridItem'; import { DashboardScene } from './DashboardScene'; import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks'; import { panelMenuBehavior } from './PanelMenuBehavior'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; const mocks = { contextSrv: jest.mocked(contextSrv), @@ -588,18 +587,7 @@ async function buildTestScene(options: SceneOptions) { canEdit: true, isEmbedded: options.isEmbedded ?? false, }, - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), }); await new Promise((r) => setTimeout(r, 1)); diff --git a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx index f9cd350a9a7..a5b3c0a0b81 100644 --- a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx @@ -21,6 +21,7 @@ import { DashboardGridItem, RepeatDirection } from './DashboardGridItem'; import { DashboardScene } from './DashboardScene'; import { panelMenuBehavior, repeatPanelMenuBehavior } from './PanelMenuBehavior'; import { RowRepeaterBehavior } from './RowRepeaterBehavior'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; import { RowActions } from './row-actions/RowActions'; jest.mock('@grafana/runtime', () => ({ @@ -350,7 +351,7 @@ function buildScene( }), ], }), - body: grid, + body: new DefaultGridLayoutManager({ grid }), }); const rowToRepeat = repeatBehavior.parent as SceneGridRow; diff --git a/public/app/features/dashboard-scene/scene/ViewPanelScene.test.tsx b/public/app/features/dashboard-scene/scene/ViewPanelScene.test.tsx index 5919e5c432c..47f6a3307d0 100644 --- a/public/app/features/dashboard-scene/scene/ViewPanelScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/ViewPanelScene.test.tsx @@ -3,6 +3,7 @@ import { LocalValueVariable, SceneGridLayout, SceneGridRow, SceneVariableSet, Vi import { DashboardGridItem } from './DashboardGridItem'; import { DashboardScene } from './DashboardScene'; import { ViewPanelScene } from './ViewPanelScene'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), @@ -32,23 +33,25 @@ function buildScene(options?: SceneOptions) { }); const dashboard = new DashboardScene({ - body: new SceneGridLayout({ - children: [ - new SceneGridRow({ - x: 0, - y: 10, - width: 24, - $variables: new SceneVariableSet({ - variables: [new LocalValueVariable({ value: 'row-var-value' })], - }), - height: 1, - children: [ - new DashboardGridItem({ - body: panel, + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [ + new SceneGridRow({ + x: 0, + y: 10, + width: 24, + $variables: new SceneVariableSet({ + variables: [new LocalValueVariable({ value: 'row-var-value' })], }), - ], - }), - ], + height: 1, + children: [ + new DashboardGridItem({ + body: panel, + }), + ], + }), + ], + }), }), }); diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts index 4e025631e01..10fc2feac74 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts @@ -14,6 +14,7 @@ import { getPanelIdForVizPanel } from '../utils/utils'; import { DashboardScene } from './DashboardScene'; import { onRemovePanel, toggleVizPanelLegend } from './PanelMenuBehavior'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; export function setupKeyboardShortcuts(scene: DashboardScene) { const keybindings = new KeybindingSet(); @@ -214,7 +215,9 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { keybindings.addBinding({ key: 'd shift+c', onTrigger: () => { - scene.collapseAllRows(); + if (scene.state.body instanceof DefaultGridLayoutManager) { + scene.state.body.collapseAllRows(); + } }, }); @@ -222,7 +225,9 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { keybindings.addBinding({ key: 'd shift+e', onTrigger: () => { - scene.expandAllRows(); + if (scene.state.body instanceof DefaultGridLayoutManager) { + scene.state.body.expandAllRows(); + } }, }); } 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 new file mode 100644 index 00000000000..37f25cc54d0 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx @@ -0,0 +1,281 @@ +import { SceneGridItemLike, SceneGridLayout, SceneGridRow, SceneQueryRunner, VizPanel } from '@grafana/scenes'; + +import { findVizPanelByKey } from '../../utils/utils'; +import { DashboardGridItem } from '../DashboardGridItem'; + +import { DefaultGridLayoutManager } from './DefaultGridLayoutManager'; + +describe('DefaultGridLayoutManager', () => { + describe('getVizPanels', () => { + it('Should return all panels', () => { + const { manager } = setup(); + const vizPanels = manager.getVizPanels(); + + expect(vizPanels.length).toBe(4); + expect(vizPanels[0].state.title).toBe('Panel A'); + expect(vizPanels[1].state.title).toBe('Panel B'); + expect(vizPanels[2].state.title).toBe('Panel C'); + expect(vizPanels[3].state.title).toBe('Panel D'); + }); + + it('Should return an empty array when scene has no panels', () => { + const { manager } = setup({ gridItems: [] }); + const vizPanels = manager.getVizPanels(); + expect(vizPanels.length).toBe(0); + }); + }); + + describe('getNextPanelId', () => { + it('should get next panel id in a simple 3 panel layout', () => { + const { manager } = setup(); + const id = manager.getNextPanelId(); + + expect(id).toBe(4); + }); + + it('should return 1 if no panels are found', () => { + const { manager } = setup({ gridItems: [] }); + const id = manager.getNextPanelId(); + + expect(id).toBe(1); + }); + }); + + describe('addPanel', () => { + it('Should add a new panel', () => { + const { manager } = setup(); + + const vizPanel = new VizPanel({ + title: 'Panel Title', + key: 'panel-55', + pluginId: 'timeseries', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }); + + manager.addPanel(vizPanel); + + const panel = findVizPanelByKey(manager, 'panel-55')!; + const gridItem = panel.parent as DashboardGridItem; + + expect(panel).toBeDefined(); + expect(gridItem.state.y).toBe(0); + }); + }); + + describe('addNewRow', () => { + it('Should create and add a new row to the dashboard', () => { + const { manager, grid } = setup(); + const row = manager.addNewRow(); + + expect(grid.state.children.length).toBe(2); + expect(row.state.key).toBe('panel-4'); + expect(row.state.children[0].state.key).toBe('griditem-1'); + expect(row.state.children[1].state.key).toBe('griditem-2'); + }); + + it('Should create a row and add all panels in the dashboard under it', () => { + const { manager, grid } = setup({ + gridItems: [ + new DashboardGridItem({ + key: 'griditem-1', + x: 0, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }), + }), + new DashboardGridItem({ + key: 'griditem-2', + body: new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', + }), + }), + ], + }); + + const row = manager.addNewRow(); + + expect(grid.state.children.length).toBe(1); + expect(row.state.children.length).toBe(2); + }); + + it('Should create and add two new rows, but the second has no children', () => { + const { manager, grid } = setup(); + const row1 = manager.addNewRow(); + const row2 = manager.addNewRow(); + + expect(grid.state.children.length).toBe(3); + expect(row1.state.children.length).toBe(2); + expect(row2.state.children.length).toBe(0); + }); + + it('Should create an empty row when nothing else in dashboard', () => { + const { manager, grid } = setup({ gridItems: [] }); + const row = manager.addNewRow(); + + expect(grid.state.children.length).toBe(1); + expect(row.state.children.length).toBe(0); + }); + }); + + describe('Remove row', () => { + it('Should remove a row and move its children to the grid layout', () => { + const { manager, grid } = setup(); + const row = grid.state.children[2] as SceneGridRow; + + manager.removeRow(row); + + expect(grid.state.children.length).toBe(4); + }); + + it('Should remove a row and its children', () => { + const { manager, grid } = setup(); + const row = grid.state.children[2] as SceneGridRow; + + manager.removeRow(row, true); + + expect(grid.state.children.length).toBe(2); + }); + + it('Should remove an empty row from the layout', () => { + const row = new SceneGridRow({ key: 'panel-1' }); + const { manager, grid } = setup({ gridItems: [row] }); + + manager.removeRow(row); + + expect(grid.state.children.length).toBe(0); + }); + }); + + describe('removePanel', () => { + it('Should remove grid item', () => { + const { manager } = setup(); + const panel = findVizPanelByKey(manager, 'panel-1')!; + manager.removePanel(panel); + + expect(findVizPanelByKey(manager, 'panel-1')).toBeNull(); + }); + + it('Should remove a grid item within a row', () => { + const { manager, grid } = setup(); + const vizPanel = findVizPanelByKey(manager, 'panel-within-row1')!; + + manager.removePanel(vizPanel); + + const gridRow = grid.state.children[2] as SceneGridRow; + expect(gridRow.state.children.length).toBe(1); + }); + }); + + describe('duplicatePanel', () => { + it('Should duplicate a panel', () => { + const { manager, grid } = setup(); + const vizPanel = findVizPanelByKey(manager, 'panel-1')!; + + expect(grid.state.children.length).toBe(3); + + manager.duplicatePanel(vizPanel); + + const newGridItem = grid.state.children[3]; + + expect(grid.state.children.length).toBe(4); + expect(newGridItem.state.key).toBe('grid-item-4'); + }); + + it('Should maintain size of duplicated panel', () => { + const { manager, grid } = setup(); + + const gItem = grid.state.children[0] as DashboardGridItem; + gItem.setState({ height: 1 }); + + const vizPanel = gItem.state.body; + manager.duplicatePanel(vizPanel); + + const newGridItem = grid.state.children[grid.state.children.length - 1] as DashboardGridItem; + + expect(newGridItem.state.height).toBe(1); + }); + + it('Should duplicate a repeated panel', () => { + const { manager, grid } = setup(); + const gItem = grid.state.children[0] as DashboardGridItem; + gItem.setState({ variableName: 'server', repeatDirection: 'v', maxPerRow: 100 }); + const vizPanel = gItem.state.body; + manager.duplicatePanel(vizPanel as VizPanel); + + const newGridItem = grid.state.children[grid.state.children.length - 1] as DashboardGridItem; + + expect(newGridItem.state.variableName).toBe('server'); + expect(newGridItem.state.repeatDirection).toBe('v'); + expect(newGridItem.state.maxPerRow).toBe(100); + }); + + it('Should duplicate a panel in a row', () => { + const { manager } = setup(); + const vizPanel = findVizPanelByKey(manager, 'panel-within-row1')!; + const gridRow = vizPanel.parent?.parent as SceneGridRow; + + expect(gridRow.state.children.length).toBe(2); + + manager.duplicatePanel(vizPanel); + + expect(gridRow.state.children.length).toBe(3); + }); + }); +}); + +interface TestOptions { + gridItems: SceneGridItemLike[]; +} + +function setup(options?: TestOptions) { + const gridItems = options?.gridItems ?? [ + new DashboardGridItem({ + key: 'griditem-1', + x: 0, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }), + }), + new DashboardGridItem({ + key: 'griditem-2', + body: new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', + }), + }), + new SceneGridRow({ + key: 'panel-3', + title: 'row', + children: [ + new DashboardGridItem({ + body: new VizPanel({ + title: 'Panel C', + key: 'panel-within-row1', + pluginId: 'table', + }), + }), + new DashboardGridItem({ + body: new VizPanel({ + title: 'Panel D', + key: 'panel-within-row2', + pluginId: 'table', + }), + }), + ], + }), + ]; + + const grid = new SceneGridLayout({ children: gridItems }); + const manager = new DefaultGridLayoutManager({ grid: grid }); + + return { manager, grid }; +} diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx new file mode 100644 index 00000000000..6728a02e669 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -0,0 +1,355 @@ +import { + SceneObjectState, + SceneGridLayout, + SceneObjectBase, + SceneGridRow, + VizPanel, + sceneGraph, + sceneUtils, + SceneComponentProps, +} from '@grafana/scenes'; +import { GRID_COLUMN_COUNT } from 'app/core/constants'; + +import { + forceRenderChildren, + getPanelIdForVizPanel, + NEW_PANEL_HEIGHT, + NEW_PANEL_WIDTH, + getVizPanelKeyForPanelId, +} from '../../utils/utils'; +import { DashboardGridItem } from '../DashboardGridItem'; +import { RowRepeaterBehavior } from '../RowRepeaterBehavior'; +import { RowActions } from '../row-actions/RowActions'; +import { DashboardLayoutManager } from '../types'; + +interface DefaultGridLayoutManagerState extends SceneObjectState { + grid: SceneGridLayout; +} + +/** + * State manager for the default grid layout + */ +export class DefaultGridLayoutManager + extends SceneObjectBase + implements DashboardLayoutManager +{ + public editModeChanged(isEditing: boolean): void { + this.state.grid.setState({ isDraggable: isEditing, isResizable: isEditing }); + forceRenderChildren(this.state.grid, true); + } + + /** + * Removes the first panel + */ + public cleanUpStateFromExplore(): void { + this.state.grid.setState({ + children: this.state.grid.state.children.slice(1), + }); + } + + public addPanel(vizPanel: VizPanel): void { + const panelId = getPanelIdForVizPanel(vizPanel); + const newGridItem = new DashboardGridItem({ + height: NEW_PANEL_HEIGHT, + width: NEW_PANEL_WIDTH, + x: 0, + y: 0, + body: vizPanel, + key: `grid-item-${panelId}`, + }); + + this.state.grid.setState({ + children: [newGridItem, ...this.state.grid.state.children], + }); + } + + /** + * Adds a new emtpy row + */ + public addNewRow(): SceneGridRow { + const id = this.getNextPanelId(); + const row = new SceneGridRow({ + key: getVizPanelKeyForPanelId(id), + title: 'Row title', + actions: new RowActions({}), + y: 0, + }); + + const sceneGridLayout = this.state.grid; + + // find all panels until the first row and put them into the newly created row. If there are no other rows, + // add all panels to the row. If there are no panels just create an empty row + const indexTillNextRow = sceneGridLayout.state.children.findIndex((child) => child instanceof SceneGridRow); + const rowChildren = sceneGridLayout.state.children + .splice(0, indexTillNextRow === -1 ? sceneGridLayout.state.children.length : indexTillNextRow) + .map((child) => child.clone()); + + if (rowChildren) { + row.setState({ children: rowChildren }); + } + + sceneGridLayout.setState({ children: [row, ...sceneGridLayout.state.children] }); + + return row; + } + + /** + * Removes a row + * @param row + * @param removePanels + */ + public removeRow(row: SceneGridRow, removePanels = false) { + const sceneGridLayout = this.state.grid; + + const children = sceneGridLayout.state.children.filter((child) => child.state.key !== row.state.key); + + if (!removePanels) { + const rowChildren = row.state.children.map((child) => child.clone()); + const indexOfRow = sceneGridLayout.state.children.findIndex((child) => child.state.key === row.state.key); + + children.splice(indexOfRow, 0, ...rowChildren); + } + + sceneGridLayout.setState({ children }); + } + + /** + * Removes a panel + */ + public removePanel(panel: VizPanel) { + const gridItem = panel.parent!; + + if (!(gridItem instanceof DashboardGridItem)) { + throw new Error('Trying to remove panel that is not inside a DashboardGridItem'); + } + + const layout = this.state.grid; + + let row: SceneGridRow | undefined; + + try { + row = sceneGraph.getAncestor(gridItem, SceneGridRow); + } catch { + row = undefined; + } + + if (row) { + row.setState({ children: row.state.children.filter((child) => child !== gridItem) }); + layout.forceRender(); + return; + } + + this.state.grid.setState({ + children: layout.state.children.filter((child) => child !== gridItem), + }); + } + + public duplicatePanel(vizPanel: VizPanel): void { + const gridItem = vizPanel.parent; + if (!(gridItem instanceof DashboardGridItem)) { + console.error('Trying to duplicate a panel that is not inside a DashboardGridItem'); + return; + } + + let panelState; + let panelData; + let newGridItem; + + const newPanelId = this.getNextPanelId(); + const grid = this.state.grid; + + if (gridItem instanceof DashboardGridItem) { + panelState = sceneUtils.cloneSceneObjectState(gridItem.state.body.state); + panelData = sceneGraph.getData(gridItem.state.body).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 DashboardGridItem({ + x: gridItem.state.x, + y: gridItem.state.y, + height: gridItem.state.height, + width: gridItem.state.width, + variableName: gridItem.state.variableName, + repeatDirection: gridItem.state.repeatDirection, + maxPerRow: gridItem.state.maxPerRow, + key: `grid-item-${newPanelId}`, + body: new VizPanel({ ...panelState, $data: panelData, key: getVizPanelKeyForPanelId(newPanelId) }), + }); + + if (gridItem.parent instanceof SceneGridRow) { + const row = gridItem.parent; + + row.setState({ children: [...row.state.children, newGridItem] }); + + grid.forceRender(); + return; + } + + grid.setState({ children: [...grid.state.children, newGridItem] }); + } + + public getVizPanels(): VizPanel[] { + const panels: VizPanel[] = []; + + this.state.grid.forEachChild((child) => { + if (!(child instanceof DashboardGridItem) && !(child instanceof SceneGridRow)) { + throw new Error('Child is not a DashboardGridItem or SceneGridRow, invalid scene'); + } + + if (child instanceof DashboardGridItem) { + if (child.state.body instanceof VizPanel) { + panels.push(child.state.body); + } + } else if (child instanceof SceneGridRow) { + child.forEachChild((child) => { + if (child instanceof DashboardGridItem) { + if (child.state.body instanceof VizPanel) { + panels.push(child.state.body); + } + } + }); + } + }); + + return panels; + } + + public getNextPanelId(): number { + let max = 0; + + for (const child of this.state.grid.state.children) { + if (child instanceof DashboardGridItem) { + const vizPanel = child.state.body; + + if (vizPanel) { + const panelId = getPanelIdForVizPanel(vizPanel); + + if (panelId > max) { + max = panelId; + } + } + } + + if (child instanceof SceneGridRow) { + //rows follow the same key pattern --- e.g.: `panel-6` + const panelId = getPanelIdForVizPanel(child); + + if (panelId > max) { + max = panelId; + } + + for (const rowChild of child.state.children) { + if (rowChild instanceof DashboardGridItem) { + const vizPanel = rowChild.state.body; + + if (vizPanel) { + const panelId = getPanelIdForVizPanel(vizPanel); + + if (panelId > max) { + max = panelId; + } + } + } + } + } + } + + return max + 1; + } + + public collapseAllRows() { + this.state.grid.state.children.forEach((child) => { + if (!(child instanceof SceneGridRow)) { + return; + } + if (!child.state.isCollapsed) { + this.state.grid.toggleRow(child); + } + }); + } + + public expandAllRows() { + this.state.grid.state.children.forEach((child) => { + if (!(child instanceof SceneGridRow)) { + return; + } + if (child.state.isCollapsed) { + this.state.grid.toggleRow(child); + } + }); + } + + activateRepeaters(): void { + this.state.grid.forEachChild((child) => { + if (child instanceof DashboardGridItem && !child.isActive) { + child.activate(); + return; + } + + if (child instanceof SceneGridRow && child.state.$behaviors) { + for (const behavior of child.state.$behaviors) { + if (behavior instanceof RowRepeaterBehavior && !child.isActive) { + child.activate(); + break; + } + } + + child.state.children.forEach((child) => { + if (child instanceof DashboardGridItem && !child.isActive) { + child.activate(); + return; + } + }); + } + }); + } + + /** + * For simple test grids + * @param panels + */ + public static fromVizPanels(panels: VizPanel[] = []): DefaultGridLayoutManager { + const children: DashboardGridItem[] = []; + const panelHeight = 10; + const panelWidth = GRID_COLUMN_COUNT / 3; + let currentY = 0; + let currentX = 0; + + for (let panel of panels) { + children.push( + new DashboardGridItem({ + key: `griditem-${getPanelIdForVizPanel(panel)}`, + x: currentX, + y: currentY, + width: panelWidth, + height: panelHeight, + body: panel, + }) + ); + + currentX += panelWidth; + + if (currentX + panelWidth >= GRID_COLUMN_COUNT) { + currentX = 0; + currentY += panelHeight; + } + } + + return new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: children, + isDraggable: false, + isResizable: false, + }), + }); + } + + public static Component = ({ model }: SceneComponentProps) => { + return ; + }; +} diff --git a/public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx b/public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx index 9dadfd0cbad..a1cc833602b 100644 --- a/public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx +++ b/public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx @@ -1,7 +1,14 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { SceneComponentProps, SceneGridRow, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; +import { + SceneComponentProps, + sceneGraph, + SceneGridRow, + SceneObjectBase, + SceneObjectState, + VizPanel, +} from '@grafana/scenes'; import { Icon, TextLink, useStyles2 } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; @@ -11,6 +18,7 @@ import { getDashboardSceneFor, getQueryRunnerFor } from '../../utils/utils'; import { DashboardGridItem } from '../DashboardGridItem'; import { DashboardScene } from '../DashboardScene'; import { RowRepeaterBehavior } from '../RowRepeaterBehavior'; +import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager'; import { RowOptionsButton } from './RowOptionsButton'; @@ -29,6 +37,11 @@ export class RowActions extends SceneObjectBase { return getDashboardSceneFor(this); } + public removeRow(removePanels?: boolean) { + const manager = sceneGraph.getAncestor(this, DefaultGridLayoutManager); + manager.removeRow(this.getParent(), removePanels); + } + public onUpdate = (title: string, repeat?: string | null): void => { const row = this.getParent(); let repeatBehavior: RowRepeaterBehavior | undefined; @@ -60,12 +73,8 @@ export class RowActions extends SceneObjectBase { text: 'Are you sure you want to remove this row and all its panels?', altActionText: 'Delete row only', icon: 'trash-alt', - onConfirm: () => { - this.getDashboard().removeRow(this.getParent(), true); - }, - onAltAction: () => { - this.getDashboard().removeRow(this.getParent()); - }, + onConfirm: () => this.removeRow(true), + onAltAction: () => this.removeRow(), }) ); }; diff --git a/public/app/features/dashboard-scene/scene/types.ts b/public/app/features/dashboard-scene/scene/types.ts index 9bf602cc171..37a932ceb20 100644 --- a/public/app/features/dashboard-scene/scene/types.ts +++ b/public/app/features/dashboard-scene/scene/types.ts @@ -1,5 +1,89 @@ -import { BusEventWithPayload } from '@grafana/data'; -import { SceneObject } from '@grafana/scenes'; +import { BusEventWithPayload, RegistryItem } from '@grafana/data'; +import { SceneObject, VizPanel } from '@grafana/scenes'; + +/** + * A scene object that usually wraps an underlying layout + * Dealing with all the state management and editing of the layout + */ +export interface DashboardLayoutManager extends SceneObject { + /** + * Notify the layout manager that the edit mode has changed + * @param isEditing + */ + editModeChanged(isEditing: boolean): void; + /** + * We should be able to figure out how to add the explore panel in a way that leaves the + * initialSaveModel clean from it so we can leverage the default discard changes logic. + * Then we can get rid of this. + */ + cleanUpStateFromExplore?(): void; + /** + * Not sure we will need this in the long run, we should be able to handle this inside internally + */ + getNextPanelId(): number; + /** + * Remove an elemenet / panel + * @param element + */ + removePanel(panel: VizPanel): void; + /** + * Creates a copy of an existing element and adds it to the layout + * @param element + */ + duplicatePanel(panel: VizPanel): void; + /** + * Adds a new panel to the layout + */ + addPanel(panel: VizPanel): void; + /** + * Add row + */ + addNewRow(): void; + /** + * getVizPanels + */ + getVizPanels(): VizPanel[]; + /** + * Turn into a save model + * @param saveModel + */ + toSaveModel?(): any; + /** + * For dynamic panels that need to be viewed in isolation (SoloRoute) + */ + activateRepeaters?(): void; +} + +/** + * The layout descriptor used when selecting / switching layouts + */ +export interface LayoutRegistryItem extends RegistryItem { + /** + * When switching between layouts + * @param currentLayout + */ + createFromLayout(currentLayout: DashboardLayoutManager): DashboardLayoutManager; + /** + * Create from persisted state + * @param saveModel + */ + createFromSaveModel?(saveModel: any): void; +} + +export interface LayoutEditorProps { + layoutManager: T; +} + +/** + * This interface is needed to support layouts existing on different levels of the scene (DashboardScene and inside the TabsLayoutManager) + */ +export interface LayoutParent extends SceneObject { + switchLayout(newLayout: DashboardLayoutManager): void; +} + +export function isLayoutParent(obj: SceneObject): obj is LayoutParent { + return 'switchLayout' in obj; +} export interface DashboardRepeatsProcessedEventPayload { source: SceneObject; diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index 868dfd24768..37badeff929 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -31,6 +31,7 @@ import { DashboardGridItem } from '../scene/DashboardGridItem'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { PanelTimeRange } from '../scene/PanelTimeRange'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { NEW_LINK } from '../settings/links/utils'; import { getQueryRunnerFor } from '../utils/utils'; @@ -323,7 +324,8 @@ describe('transformSaveModelToScene', () => { const oldModel = new DashboardModel(dashboard); const scene = createDashboardSceneFromDashboardModel(oldModel, dashboard); - const body = scene.state.body as SceneGridLayout; + const layout = scene.state.body as DefaultGridLayoutManager; + const body = layout.state.grid; expect(body.state.children).toHaveLength(3); const rowScene1 = body.state.children[0] as SceneGridRow; @@ -375,7 +377,8 @@ describe('transformSaveModelToScene', () => { const oldModel = new DashboardModel(dashboard); const scene = createDashboardSceneFromDashboardModel(oldModel, dashboard); - const body = scene.state.body as SceneGridLayout; + const layout = scene.state.body as DefaultGridLayoutManager; + const body = layout.state.grid; expect(body.state.children).toHaveLength(1); const rowScene = body.state.children[0] as SceneGridRow; @@ -471,7 +474,8 @@ describe('transformSaveModelToScene', () => { const oldModel = new DashboardModel(dashboard); const scene = createDashboardSceneFromDashboardModel(oldModel, dashboard); - const body = scene.state.body as SceneGridLayout; + const layout = scene.state.body as DefaultGridLayoutManager; + const body = layout.state.grid; expect(body.state.children).toHaveLength(4); expect(body).toBeInstanceOf(SceneGridLayout); @@ -750,7 +754,9 @@ describe('transformSaveModelToScene', () => { dashboard: repeatingRowsAndPanelsDashboardJson as DashboardDataDTO, meta: {}, }); - const body = scene.state.body as SceneGridLayout; + + const layout = scene.state.body as DefaultGridLayoutManager; + const body = layout.state.grid; const row2 = body.state.children[1] as SceneGridRow; expect(row2.state.$behaviors?.[0]).toBeInstanceOf(RowRepeaterBehavior); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 0195625f24b..b5237a83683 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -37,6 +37,7 @@ import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavio import { PanelNotices } from '../scene/PanelNotices'; import { PanelTimeRange } from '../scene/PanelTimeRange'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { RowActions } from '../scene/row-actions/RowActions'; import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; import { createPanelDataProvider } from '../utils/createPanelDataProvider'; @@ -221,10 +222,12 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, title: oldModel.title, uid: oldModel.uid, version: oldModel.version, - body: new SceneGridLayout({ - isLazy: dto.preload ? false : true, - children: createSceneObjectsForPanels(oldModel.panels), - $behaviors: [trackIfEmpty], + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + isLazy: dto.preload ? false : true, + children: createSceneObjectsForPanels(oldModel.panels), + $behaviors: [trackIfEmpty], + }), }), $timeRange: new SceneTimeRange({ from: oldModel.time.from, diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts index eef3e336c16..bbc6d3dbb7a 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts @@ -15,7 +15,7 @@ import { } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { getPluginLinkExtensions, setPluginImportUtils } from '@grafana/runtime'; -import { MultiValueVariable, sceneGraph, SceneGridLayout, SceneGridRow, VizPanel } from '@grafana/scenes'; +import { MultiValueVariable, sceneGraph, SceneGridRow, VizPanel } from '@grafana/scenes'; import { Dashboard, LoadingState, Panel, RowPanel, VariableRefresh } from '@grafana/schema'; import { PanelModel } from 'app/features/dashboard/state'; import { getTimeRange } from 'app/features/dashboard/utils/timeRange'; @@ -27,6 +27,7 @@ import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardGridItem } from '../scene/DashboardGridItem'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { NEW_LINK } from '../settings/links/utils'; import { activateFullSceneTree, buildPanelRepeaterScene } from '../utils/test-utils'; import { getVizPanelKeyForPanelId } from '../utils/utils'; @@ -225,7 +226,8 @@ describe('transformSceneToSaveModel', () => { const variable = scene.state.$variables?.state.variables[0] as MultiValueVariable; variable.changeValueTo(['a', 'b', 'c']); - const grid = scene.state.body as SceneGridLayout; + const layout = scene.state.body as DefaultGridLayoutManager; + const grid = layout.state.grid; const rowWithRepeat = grid.state.children[1] as SceneGridRow; const rowRepeater = rowWithRepeat.state.$behaviors![0] as RowRepeaterBehavior; diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index 01605ed28ab..86aebd5ee65 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -4,7 +4,6 @@ import { isEmptyObject, ScopedVars, TimeRange } from '@grafana/data'; import { behaviors, SceneGridItemLike, - SceneGridLayout, SceneGridRow, VizPanel, SceneDataTransformer, @@ -36,6 +35,7 @@ import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene } from '../scene/DashboardScene'; import { PanelTimeRange } from '../scene/PanelTimeRange'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getLibraryPanelBehavior, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils'; @@ -53,8 +53,8 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa let panels: Panel[] = []; let variables: VariableModel[] = []; - if (body instanceof SceneGridLayout) { - for (const child of body.state.children) { + if (body instanceof DefaultGridLayoutManager) { + for (const child of body.state.grid.state.children) { if (child instanceof DashboardGridItem) { // handle panel repeater scenario if (child.state.variableName) { diff --git a/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx b/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx index d464023a07a..547e4d8e41b 100644 --- a/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx @@ -11,7 +11,7 @@ import { LoadingState, PanelData, } from '@grafana/data'; -import { SceneGridLayout, SceneTimeRange, dataLayers } from '@grafana/scenes'; +import { SceneTimeRange, dataLayers } from '@grafana/scenes'; import { DataSourceRef } from '@grafana/schema'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; @@ -221,9 +221,6 @@ async function buildTestScene() { }), ], }), - body: new SceneGridLayout({ - children: [], - }), editview: annotationsView, }); diff --git a/public/app/features/dashboard-scene/settings/DashboardLinksEditView.test.tsx b/public/app/features/dashboard-scene/settings/DashboardLinksEditView.test.tsx index 7e25117a5e8..5cbd37a99fb 100644 --- a/public/app/features/dashboard-scene/settings/DashboardLinksEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/DashboardLinksEditView.test.tsx @@ -2,7 +2,7 @@ import { render as RTLRender } from '@testing-library/react'; import * as React from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; -import { SceneGridLayout, SceneTimeRange, behaviors } from '@grafana/scenes'; +import { SceneTimeRange, behaviors } from '@grafana/scenes'; import { DashboardCursorSync } from '@grafana/schema'; import { DashboardControls } from '../scene/DashboardControls'; @@ -212,9 +212,6 @@ async function buildTestScene() { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [], - }), editview: settings, }); diff --git a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx index e2f343c383c..a7dcd617186 100644 --- a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx @@ -1,4 +1,4 @@ -import { behaviors, SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; +import { behaviors, SceneTimeRange } from '@grafana/scenes'; import { DashboardCursorSync } from '@grafana/schema'; import * as utils from '../pages/utils'; @@ -125,9 +125,6 @@ async function buildTestScene() { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [], - }), editview: settings, }); diff --git a/public/app/features/dashboard-scene/settings/PermissionsEditView.test.tsx b/public/app/features/dashboard-scene/settings/PermissionsEditView.test.tsx index 4bb9868502f..dd6e97e6594 100644 --- a/public/app/features/dashboard-scene/settings/PermissionsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/PermissionsEditView.test.tsx @@ -1,4 +1,4 @@ -import { SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; +import { SceneTimeRange } from '@grafana/scenes'; import { DashboardScene } from '../scene/DashboardScene'; import { activateFullSceneTree } from '../utils/test-utils'; @@ -36,9 +36,6 @@ async function buildTestScene() { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [], - }), editview: permissionsView, }); diff --git a/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx b/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx index a547fa10d4f..f3d93596d03 100644 --- a/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx @@ -13,7 +13,6 @@ import { setPluginImportUtils, setRunRequest } from '@grafana/runtime'; import { SceneVariableSet, CustomVariable, - SceneGridLayout, VizPanel, AdHocFiltersVariable, SceneVariableState, @@ -22,8 +21,8 @@ import { import { mockDataSource } from 'app/features/alerting/unified/mocks'; import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor'; -import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene } from '../scene/DashboardScene'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { activateFullSceneTree } from '../utils/test-utils'; import { VariablesEditView } from './VariablesEditView'; @@ -288,8 +287,7 @@ describe('VariablesEditView', () => { it('should keep dependencies with panels when the type is changed so the variable is replaced', async () => { // Uses function to avoid store reference to previous existing variables const getSourceVariable = () => variableView.getVariables()[0] as CustomVariable; - const getDependantPanel = () => - ((dashboard.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state.body as VizPanel; + const getDependantPanel = () => dashboard.state.body.getVizPanels()[0]; expect(getSourceVariable().getValue()).toBe('test'); // Using description to get the interpolated value @@ -342,20 +340,14 @@ async function buildTestScene() { }), ], }), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel A', - description: 'Panel A depends on customVar with current value $customVar', - key: 'panel-1', - pluginId: 'table', - }), - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([ + new VizPanel({ + title: 'Panel A', + description: 'Panel A depends on customVar with current value $customVar', + key: 'panel-1', + pluginId: 'table', + }), + ]), editview: variableView, }); diff --git a/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx b/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx index 9e69917dfee..6d2b37219f6 100644 --- a/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx @@ -1,4 +1,4 @@ -import { SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; +import { SceneTimeRange } from '@grafana/scenes'; import { DashboardScene } from '../scene/DashboardScene'; import { activateFullSceneTree } from '../utils/test-utils'; @@ -162,9 +162,6 @@ async function buildTestScene() { meta: { canEdit: true, }, - body: new SceneGridLayout({ - children: [], - }), editview: versionsView, }); diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.test.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.test.tsx index 4bc553b457c..22c5d54e16f 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.test.tsx @@ -2,10 +2,10 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; -import { SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes'; +import { SceneTimeRange, VizPanel } from '@grafana/scenes'; -import { DashboardGridItem } from '../../scene/DashboardGridItem'; import { DashboardScene } from '../../scene/DashboardScene'; +import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; import ExportButton from './ExportButton'; @@ -38,18 +38,7 @@ function setup() { title: 'hello', uid: 'dash-1', $timeRange: new SceneTimeRange({}), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), }); render(); diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.test.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.test.tsx index 38045ba05b0..530dd8ae765 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.test.tsx @@ -1,10 +1,10 @@ import { render, screen } from '@testing-library/react'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; -import { SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes'; +import { SceneTimeRange, VizPanel } from '@grafana/scenes'; -import { DashboardGridItem } from '../../scene/DashboardGridItem'; import { DashboardScene } from '../../scene/DashboardScene'; +import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; import ExportMenu from './ExportMenu'; @@ -27,18 +27,7 @@ function setup() { title: 'hello', uid: 'dash-1', $timeRange: new SceneTimeRange({}), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), }); render(); } diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.test.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.test.tsx index 842b257470a..f939ea8820f 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.test.tsx @@ -2,10 +2,10 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; -import { SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes'; +import { SceneTimeRange, VizPanel } from '@grafana/scenes'; -import { DashboardGridItem } from '../../scene/DashboardGridItem'; import { DashboardScene } from '../../scene/DashboardScene'; +import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; import ShareButton from './ShareButton'; @@ -44,18 +44,7 @@ function setup() { title: 'hello', uid: 'dash-1', $timeRange: new SceneTimeRange({}), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), }); render(); diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx index 525ece5f205..0e7940e34d1 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx @@ -1,12 +1,12 @@ import { render, screen } from '@testing-library/react'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; -import { SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes'; +import { SceneTimeRange, VizPanel } from '@grafana/scenes'; import { contextSrv } from 'app/core/services/context_srv'; import { config } from '../../../../core/config'; -import { DashboardGridItem } from '../../scene/DashboardGridItem'; import { DashboardScene, DashboardSceneState } from '../../scene/DashboardScene'; +import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; import ShareMenu from './ShareMenu'; @@ -87,18 +87,7 @@ function setup(overrides?: Partial) { title: 'hello', uid: 'dash-1', $timeRange: new SceneTimeRange({}), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), ...overrides, }); diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareAlerts.test.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareAlerts.test.tsx index 205bf6bc12c..2dab2d48804 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareAlerts.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareAlerts.test.tsx @@ -4,7 +4,6 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { CustomVariable, SceneDataTransformer, - SceneGridLayout, SceneQueryRunner, SceneTimeRange, SceneVariableSet, @@ -12,8 +11,8 @@ import { VizPanelState, } from '@grafana/scenes'; import { contextSrv } from 'app/core/core'; -import { DashboardGridItem } from 'app/features/dashboard-scene/scene/DashboardGridItem'; import { DashboardScene, DashboardSceneState } from 'app/features/dashboard-scene/scene/DashboardScene'; +import { DefaultGridLayoutManager } from 'app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager'; import { ShareDrawerContext } from '../../ShareDrawer/ShareDrawerContext'; @@ -91,18 +90,7 @@ async function setup(panelState?: Partial, dashboardState?: Parti title: 'hello', uid: 'dash-1', $timeRange: new SceneTimeRange({}), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), ...dashboardState, }); diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareExternally.test.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareExternally.test.tsx index 7c25a0910cf..07fdec6b6a1 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareExternally.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareExternally.test.tsx @@ -7,18 +7,17 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { config, setPluginImportUtils } from '@grafana/runtime'; import { CustomVariable, - SceneGridLayout, SceneQueryRunner, SceneTimeRange, SceneVariableSet, VizPanel, VizPanelState, } from '@grafana/scenes'; +import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; +import { DefaultGridLayoutManager } from 'app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager'; import { contextSrv } from '../../../../../core/services/context_srv'; import * as sharePublicDashboardUtils from '../../../../dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; -import { shareDashboardType } from '../../../../dashboard/components/ShareModal/utils'; -import { DashboardGridItem } from '../../../scene/DashboardGridItem'; import { DashboardScene, DashboardSceneState } from '../../../scene/DashboardScene'; import { activateFullSceneTree } from '../../../utils/test-utils'; import { ShareDrawer } from '../../ShareDrawer/ShareDrawer'; @@ -67,6 +66,7 @@ describe('Alerts', () => { }); expect(screen.queryByTestId(selectors.TemplateVariablesWarningAlert)).toBeInTheDocument(); }); + it('when dashboard has unsupported datasources, warning is shown', async () => { await buildAndRenderScenario({ panelOverrides: { @@ -101,23 +101,14 @@ async function buildAndRenderScenario({ canEdit: true, }, $timeRange: new SceneTimeRange({}), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: new VizPanel({ - title: 'Panel A', - pluginId: 'table', - key: 'panel-12', - ...panelOverrides, - }), - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([ + new VizPanel({ + title: 'Panel A', + pluginId: 'table', + key: 'panel-12', + ...panelOverrides, + }), + ]), overlay: drawer, ...overrides, }); diff --git a/public/app/features/dashboard-scene/sharing/ShareDrawer/ShareDrawer.test.tsx b/public/app/features/dashboard-scene/sharing/ShareDrawer/ShareDrawer.test.tsx index ea2bd4f3588..b2772d5f856 100644 --- a/public/app/features/dashboard-scene/sharing/ShareDrawer/ShareDrawer.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareDrawer/ShareDrawer.test.tsx @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { selectors } from '@grafana/e2e-selectors'; import { locationService, setPluginImportUtils } from '@grafana/runtime'; -import { SceneGridLayout, SceneTimeRange, UrlSyncContextProvider } from '@grafana/scenes'; +import { SceneTimeRange, UrlSyncContextProvider } from '@grafana/scenes'; import { render } from '../../../../../test/test-utils'; import { shareDashboardType } from '../../../dashboard/components/ShareModal/utils'; @@ -63,9 +63,6 @@ async function buildAndRenderScenario() { canEdit: true, }, $timeRange: new SceneTimeRange({}), - body: new SceneGridLayout({ - children: [], - }), overlay: drawer, }); diff --git a/public/app/features/dashboard-scene/sharing/ShareLinkTab.test.tsx b/public/app/features/dashboard-scene/sharing/ShareLinkTab.test.tsx index 7ae9f501f0c..cded7ef3abe 100644 --- a/public/app/features/dashboard-scene/sharing/ShareLinkTab.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareLinkTab.test.tsx @@ -6,10 +6,10 @@ import { dateTime } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { selectors } from '@grafana/e2e-selectors'; import { config, locationService, setPluginImportUtils } from '@grafana/runtime'; -import { SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes'; +import { SceneTimeRange, VizPanel } from '@grafana/scenes'; -import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene } from '../scene/DashboardScene'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { activateFullSceneTree } from '../utils/test-utils'; import { ShareLinkTab } from './ShareLinkTab'; @@ -111,18 +111,7 @@ function buildAndRenderScenario(options: ScenarioOptions) { canEdit: true, }, $timeRange: new SceneTimeRange({}), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - y: 0, - width: 10, - height: 12, - body: panel, - }), - ], - }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), overlay: tab, }); diff --git a/public/app/features/dashboard-scene/sharing/public-dashboards/utils.ts b/public/app/features/dashboard-scene/sharing/public-dashboards/utils.ts index 37b49dc9d29..35e0ddf223b 100644 --- a/public/app/features/dashboard-scene/sharing/public-dashboards/utils.ts +++ b/public/app/features/dashboard-scene/sharing/public-dashboards/utils.ts @@ -1,17 +1,10 @@ import { DataSourceWithBackend } from '@grafana/runtime'; -import { - SceneGridItemLike, - VizPanel, - SceneQueryRunner, - SceneDataTransformer, - SceneGridLayout, - SceneGridRow, -} from '@grafana/scenes'; +import { VizPanel } from '@grafana/scenes'; import { supportedDatasources } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SupportedPubdashDatasources'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { DashboardGridItem } from '../../scene/DashboardGridItem'; import { DashboardScene } from '../../scene/DashboardScene'; +import { getQueryRunnerFor } from '../../utils/utils'; export const getUnsupportedDashboardDatasources = async (types: string[]): Promise => { let unsupportedDS = new Set(); @@ -33,63 +26,25 @@ export const getUnsupportedDashboardDatasources = async (types: string[]): Promi export function getPanelDatasourceTypes(scene: DashboardScene): string[] { const types = new Set(); - const body = scene.state.body; - if (!(body instanceof SceneGridLayout)) { - return []; - } + const panels = scene.state.body.getVizPanels(); - for (const child of body.state.children) { - if (child instanceof DashboardGridItem) { - const ts = panelDatasourceTypes(child); - for (const t of ts) { - types.add(t); - } - } - - if (child instanceof SceneGridRow) { - const ts = rowTypes(child); - for (const t of ts) { - types.add(t); - } + for (const child of panels) { + const ts = panelDatasourceTypes(child); + for (const t of ts) { + types.add(t); } } return Array.from(types).sort(); } -function rowTypes(gridRow: SceneGridRow) { - const types = new Set(gridRow.state.children.map((c) => panelDatasourceTypes(c)).flat()); - return types; -} - -function panelDatasourceTypes(gridItem: SceneGridItemLike) { - let vizPanel: VizPanel | undefined; - - if (gridItem instanceof DashboardGridItem) { - if (gridItem.state.body instanceof VizPanel) { - vizPanel = gridItem.state.body; - } else { - throw new Error('DashboardGridItem body expected to be VizPanel'); - } - } - - if (!vizPanel) { - throw new Error('Unsupported grid item type'); - } - const dataProvider = vizPanel.state.$data; +function panelDatasourceTypes(vizPanel: VizPanel) { const types = new Set(); - if (dataProvider instanceof SceneQueryRunner) { - for (const q of dataProvider.state.queries) { - types.add(q.datasource?.type ?? ''); - } - } - if (dataProvider instanceof SceneDataTransformer) { - const panelData = dataProvider.state.$data; - if (panelData instanceof SceneQueryRunner) { - for (const q of panelData.state.queries) { - types.add(q.datasource?.type ?? ''); - } + const queryRunner = getQueryRunnerFor(vizPanel); + if (queryRunner) { + for (const q of queryRunner.state.queries) { + types.add(q.datasource?.type ?? ''); } } diff --git a/public/app/features/dashboard-scene/solo/useSoloPanel.ts b/public/app/features/dashboard-scene/solo/useSoloPanel.ts index 28d8abf22d9..8905a79ce32 100644 --- a/public/app/features/dashboard-scene/solo/useSoloPanel.ts +++ b/public/app/features/dashboard-scene/solo/useSoloPanel.ts @@ -1,10 +1,8 @@ import { useState, useEffect } from 'react'; -import { VizPanel, SceneObject, SceneGridRow, UrlSyncManager } from '@grafana/scenes'; +import { VizPanel, UrlSyncManager } from '@grafana/scenes'; -import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene } from '../scene/DashboardScene'; -import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { DashboardRepeatsProcessedEvent } from '../scene/types'; import { findVizPanelByKey, isPanelClone } from '../utils/utils'; @@ -63,31 +61,10 @@ function findRepeatClone(dashboard: DashboardScene, panelId: string): Promise { - if (child instanceof DashboardGridItem && !child.isActive) { - child.activate(); - return; - } - - if (child instanceof SceneGridRow && child.state.$behaviors) { - for (const behavior of child.state.$behaviors) { - if (behavior instanceof RowRepeaterBehavior && !child.isActive) { - child.activate(); - break; - } - } - - // Activate any panel DashboardGridItem inside the row - activateAllRepeaters(child); - } + dashboard.state.body.activateRepeaters?.(); }); } diff --git a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts index a6b43edad2e..d8a9b152b03 100644 --- a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts +++ b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts @@ -1,7 +1,6 @@ import { TimeRangeUpdatedEvent } from '@grafana/runtime'; import { behaviors, - SceneGridLayout, SceneQueryRunner, SceneTimeRange, VizPanel, @@ -14,8 +13,8 @@ import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardControls } from '../scene/DashboardControls'; -import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene } from '../scene/DashboardScene'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { NEW_LINK } from '../settings/links/utils'; import { DashboardModelCompatibilityWrapper } from './DashboardModelCompatibilityWrapper'; @@ -97,13 +96,13 @@ describe('DashboardModelCompatibilityWrapper', () => { }); it('Can remove panel', () => { - const { wrapper, scene } = setup(); + const { wrapper } = setup(); - expect((scene.state.body as SceneGridLayout).state.children.length).toBe(5); + expect(wrapper.panels.length).toBe(5); wrapper.removePanel(wrapper.getPanelById(1)!); - expect((scene.state.body as SceneGridLayout).state.children.length).toBe(4); + expect(wrapper.panels.length).toBe(4); }); it('Checks if annotations are editable', () => { @@ -173,75 +172,59 @@ function setup() { controls: new DashboardControls({ hideTimeControls: true, }), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel with a regular data source query', - key: 'panel-1', - pluginId: 'table', - $data: new SceneQueryRunner({ - key: 'data-query-runner', - queries: [{ refId: 'A' }], - datasource: { uid: 'gdev-testdata', type: 'grafana-testdata-datasource' }, - }), - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel with no queries', - key: 'panel-2', - pluginId: 'table', - }), + body: DefaultGridLayoutManager.fromVizPanels([ + new VizPanel({ + title: 'Panel with a regular data source query', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ + key: 'data-query-runner', + queries: [{ refId: 'A' }], + datasource: { uid: 'gdev-testdata', type: 'grafana-testdata-datasource' }, }), - - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel with a shared query', - key: 'panel-3', - pluginId: 'table', - $data: new SceneQueryRunner({ - key: 'data-query-runner', - queries: [{ refId: 'A', panelId: 1 }], - datasource: { uid: SHARED_DASHBOARD_QUERY, type: 'datasource' }, - }), - }), + }), + new VizPanel({ + title: 'Panel with no queries', + key: 'panel-2', + pluginId: 'table', + }), + new VizPanel({ + title: 'Panel with a shared query', + key: 'panel-3', + pluginId: 'table', + $data: new SceneQueryRunner({ + key: 'data-query-runner', + queries: [{ refId: 'A', panelId: 1 }], + datasource: { uid: SHARED_DASHBOARD_QUERY, type: 'datasource' }, }), - - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel with a regular data source query and transformations', - key: 'panel-4', - pluginId: 'table', - $data: new SceneDataTransformer({ - $data: new SceneQueryRunner({ - key: 'data-query-runner', - queries: [{ refId: 'A' }], - datasource: { uid: 'gdev-testdata', type: 'grafana-testdata-datasource' }, - }), - transformations: [], - }), + }), + new VizPanel({ + title: 'Panel with a regular data source query and transformations', + key: 'panel-4', + pluginId: 'table', + $data: new SceneDataTransformer({ + $data: new SceneQueryRunner({ + key: 'data-query-runner', + queries: [{ refId: 'A' }], + datasource: { uid: 'gdev-testdata', type: 'grafana-testdata-datasource' }, }), + transformations: [], }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel with a shared query and transformations', - key: 'panel-4', - pluginId: 'table', - $data: new SceneDataTransformer({ - $data: new SceneQueryRunner({ - key: 'data-query-runner', - queries: [{ refId: 'A', panelId: 1 }], - datasource: { uid: SHARED_DASHBOARD_QUERY, type: 'datasource' }, - }), - transformations: [], - }), + }), + new VizPanel({ + title: 'Panel with a shared query and transformations', + key: 'panel-4', + pluginId: 'table', + $data: new SceneDataTransformer({ + $data: new SceneQueryRunner({ + key: 'data-query-runner', + queries: [{ refId: 'A', panelId: 1 }], + datasource: { uid: SHARED_DASHBOARD_QUERY, type: 'datasource' }, }), + transformations: [], }), - ], - }), + }), + ]), }); const wrapper = new DashboardModelCompatibilityWrapper(scene); diff --git a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts index a5e25a93968..9140b9ce6ae 100644 --- a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts +++ b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts @@ -2,17 +2,8 @@ import { Subscription } from 'rxjs'; import { AnnotationQuery, DashboardCursorSync, dateTimeFormat, DateTimeInput, EventBusSrv } from '@grafana/data'; import { TimeRangeUpdatedEvent } from '@grafana/runtime'; -import { - behaviors, - SceneDataLayerSet, - sceneGraph, - SceneGridLayout, - SceneGridRow, - SceneObject, - VizPanel, -} from '@grafana/scenes'; - -import { DashboardGridItem } from '../scene/DashboardGridItem'; +import { behaviors, SceneDataLayerSet, sceneGraph, SceneObject, VizPanel } from '@grafana/scenes'; + import { DashboardScene } from '../scene/DashboardScene'; import { dataLayersToAnnotations } from '../serialization/dataLayersToAnnotations'; @@ -175,39 +166,7 @@ export class DashboardModelCompatibilityWrapper { return; } - const gridItem = vizPanel.parent; - if (!(gridItem instanceof DashboardGridItem)) { - console.error('Trying to remove a panel that is not wrapped in DashboardGridItem'); - return; - } - - const layout = sceneGraph.getLayout(vizPanel); - if (!(layout instanceof SceneGridLayout)) { - console.error('Trying to remove a panel in a layout that is not SceneGridLayout '); - return; - } - - // if grid item is directly in the layout just remove it - if (layout === gridItem.parent) { - layout.setState({ - children: layout.state.children.filter((child) => child !== gridItem), - }); - } - - // Removing from a row is a bit more complicated - if (gridItem.parent instanceof SceneGridRow) { - // Clone the row and remove the grid item - const newRow = layout.clone({ - children: layout.state.children.filter((child) => child !== gridItem), - }); - - // Now update the grid layout and replace the row with the updated one - if (layout.parent instanceof SceneGridLayout) { - layout.parent.setState({ - children: layout.parent.state.children.map((child) => (child === layout ? newRow : child)), - }); - } - } + this._scene.removePanel(vizPanel); } public canEditAnnotations(dashboardUID?: string) { diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts index 7e97d9530dd..28dc6622f12 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts @@ -1,4 +1,4 @@ -import { SceneGridLayout, SceneGridRow, SceneQueryRunner, SceneTimeRange, VizPanel, behaviors } from '@grafana/scenes'; +import { SceneGridLayout, SceneQueryRunner, SceneTimeRange, VizPanel, behaviors } from '@grafana/scenes'; import { DashboardCursorSync } from '@grafana/schema'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; @@ -6,10 +6,10 @@ import { DashboardControls } from '../scene/DashboardControls'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; -import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; -import { dashboardSceneGraph, getNextPanelId } from './dashboardSceneGraph'; +import { dashboardSceneGraph } from './dashboardSceneGraph'; import { findVizPanelByKey } from './utils'; describe('dashboardSceneGraph', () => { @@ -27,36 +27,6 @@ describe('dashboardSceneGraph', () => { }); }); - describe('getVizPanels', () => { - let scene: DashboardScene; - - beforeEach(async () => { - scene = buildTestScene(); - }); - - it('Should return all panels', () => { - const vizPanels = dashboardSceneGraph.getVizPanels(scene); - - expect(vizPanels.length).toBe(6); - expect(vizPanels[0].state.title).toBe('Panel A'); - expect(vizPanels[1].state.title).toBe('Panel B'); - expect(vizPanels[2].state.title).toBe('Panel C'); - expect(vizPanels[3].state.title).toBe('Panel D'); - expect(vizPanels[4].state.title).toBe('Panel E'); - expect(vizPanels[5].state.title).toBe('Panel F'); - }); - - it('Should return an empty array when scene has no panels', () => { - scene.setState({ - body: new SceneGridLayout({ children: [] }), - }); - - const vizPanels = dashboardSceneGraph.getVizPanels(scene); - - expect(vizPanels.length).toBe(0); - }); - }); - describe('getDataLayers', () => { let scene: DashboardScene; @@ -80,141 +50,6 @@ describe('dashboardSceneGraph', () => { }); }); - describe('getNextPanelId', () => { - it('should get next panel id in a simple 3 panel layout', () => { - const scene = buildTestScene({ - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel B', - key: 'panel-2', - pluginId: 'table', - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel C', - key: 'panel-3', - pluginId: 'table', - }), - }), - ], - }), - }); - - const id = getNextPanelId(scene); - - expect(id).toBe(4); - }); - - it('should take library panels, panels in rows and panel repeaters into account', () => { - const scene = buildTestScene({ - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Library Panel 1', - key: 'panel-2', - $behaviors: [ - new LibraryPanelBehavior({ - uid: 'uid', - name: 'LibPanel', - title: 'Library Panel 1', - }), - ], - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel C', - key: 'panel-2-clone-1', - pluginId: 'table', - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel C', - key: 'panel-4', - pluginId: 'table', - }), - variableName: 'repeat', - repeatedPanels: [], - repeatDirection: 'h', - maxPerRow: 1, - }), - new SceneGridRow({ - key: 'key', - title: 'row', - children: [ - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel E', - key: 'panel-2-clone-2', - pluginId: 'table', - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Library Panel 2', - key: 'panel-3', - $behaviors: [ - new LibraryPanelBehavior({ - uid: 'uid', - name: 'LibPanel', - title: 'Library Panel 2', - }), - ], - }), - }), - ], - }), - ], - }), - }); - - const id = getNextPanelId(scene); - - expect(id).toBe(5); - }); - - it('should get next panel id in a layout with rows', () => { - const scene = buildTestScene(); - const id = getNextPanelId(scene); - - expect(id).toBe(3); - }); - - it('should return 1 if no panels are found', () => { - const scene = buildTestScene({ body: new SceneGridLayout({ children: [] }) }); - const id = getNextPanelId(scene); - - expect(id).toBe(1); - }); - - it('should throw an error if body is not SceneGridLayout', () => { - const scene = buildTestScene({ body: undefined }); - - expect(() => getNextPanelId(scene)).toThrow('Dashboard body is not a SceneGridLayout'); - }); - }); - describe('getCursorSync', () => { it('should return cursor sync behavior', () => { const scene = buildTestScene(); @@ -224,7 +59,8 @@ describe('dashboardSceneGraph', () => { }); it('should return undefined if no cursor sync behavior', () => { - const scene = buildTestScene({ $behaviors: [] }); + const scene = buildTestScene(); + scene.setState({ $behaviors: [] }); const cursorSync = dashboardSceneGraph.getCursorSync(scene); expect(cursorSync).toBeUndefined(); @@ -259,63 +95,30 @@ function buildTestScene(overrides?: Partial) { }), ], }), - body: new SceneGridLayout({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel B', - key: 'panel-2', - pluginId: 'table', - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel C', - key: 'panel-2-clone-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner2', queries: [{ refId: 'A' }] }), - }), - }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel D', - key: 'panel-with-links', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner3', queries: [{ refId: 'A' }] }), - titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })], - }), - }), - new SceneGridRow({ - key: 'key', - title: 'row', - children: [ - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel E', - key: 'panel-2-clone-2', - pluginId: 'table', - }), + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [ + new DashboardGridItem({ + key: 'griditem-1', + x: 0, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), }), - new DashboardGridItem({ - body: new VizPanel({ - title: 'Panel F', - key: 'panel-2-clone-2', - pluginId: 'table', - }), + }), + new DashboardGridItem({ + body: new VizPanel({ + title: 'Panel D', + key: 'panel-with-links', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner3', queries: [{ refId: 'A' }] }), + titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })], }), - ], - }), - ], + }), + ], + }), }), ...overrides, }); diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts index 7f6198fd605..61ee5d75291 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts @@ -1,12 +1,9 @@ -import { VizPanel, SceneGridRow, sceneGraph, SceneGridLayout, behaviors } from '@grafana/scenes'; +import { VizPanel, sceneGraph, behaviors } from '@grafana/scenes'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; -import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardScene } from '../scene/DashboardScene'; import { VizPanelLinks } from '../scene/PanelLinks'; -import { getPanelIdForVizPanel } from './utils'; - function getTimePicker(scene: DashboardScene) { return scene.state.controls?.state.timePicker; } @@ -28,29 +25,7 @@ function getPanelLinks(panel: VizPanel) { } function getVizPanels(scene: DashboardScene): VizPanel[] { - const panels: VizPanel[] = []; - - scene.state.body.forEachChild((child) => { - if (!(child instanceof DashboardGridItem) && !(child instanceof SceneGridRow)) { - throw new Error('Child is not a DashboardGridItem or SceneGridRow, invalid scene'); - } - - if (child instanceof DashboardGridItem) { - if (child.state.body instanceof VizPanel) { - panels.push(child.state.body); - } - } else if (child instanceof SceneGridRow) { - child.forEachChild((child) => { - if (child instanceof DashboardGridItem) { - if (child.state.body instanceof VizPanel) { - panels.push(child.state.body); - } - } - }); - } - }); - - return panels; + return scene.state.body.getVizPanels(); } function getDataLayers(scene: DashboardScene): DashboardDataLayerSet { @@ -74,51 +49,7 @@ export function getCursorSync(scene: DashboardScene) { } export function getNextPanelId(dashboard: DashboardScene): number { - let max = 0; - const body = dashboard.state.body; - - if (!(body instanceof SceneGridLayout)) { - throw new Error('Dashboard body is not a SceneGridLayout'); - } - - for (const child of body.state.children) { - if (child instanceof DashboardGridItem) { - const vizPanel = child.state.body; - - if (vizPanel) { - const panelId = getPanelIdForVizPanel(vizPanel); - - if (panelId > max) { - max = panelId; - } - } - } - - if (child instanceof SceneGridRow) { - //rows follow the same key pattern --- e.g.: `panel-6` - const panelId = getPanelIdForVizPanel(child); - - if (panelId > max) { - max = panelId; - } - - for (const rowChild of child.state.children) { - if (rowChild instanceof DashboardGridItem) { - const vizPanel = rowChild.state.body; - - if (vizPanel) { - const panelId = getPanelIdForVizPanel(vizPanel); - - if (panelId > max) { - max = panelId; - } - } - } - } - } - } - - return max + 1; + return dashboard.state.body.getNextPanelId(); } export const dashboardSceneGraph = { diff --git a/public/app/features/dashboard-scene/utils/test-utils.ts b/public/app/features/dashboard-scene/utils/test-utils.ts index aa55c8a7eac..8e55d67d767 100644 --- a/public/app/features/dashboard-scene/utils/test-utils.ts +++ b/public/app/features/dashboard-scene/utils/test-utils.ts @@ -18,6 +18,7 @@ import { DashboardDTO } from 'app/types'; import { DashboardGridItem, RepeatDirection } from '../scene/DashboardGridItem'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; export function setupLoadDashboardMock(rsp: DeepPartial, spy?: jest.Mock) { const loadDashboardMock = (spy || jest.fn()).mockResolvedValue(rsp); @@ -184,8 +185,10 @@ export function buildPanelRepeaterScene(options: SceneOptions, source?: VizPanel $variables: new SceneVariableSet({ variables: [panelRepeatVariable, rowRepeatVariable], }), - body: new SceneGridLayout({ - children: [row], + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [row], + }), }), }); diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 004e152bc4c..a43bfa336ad 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -6,7 +6,6 @@ import { MultiValueVariable, SceneDataTransformer, sceneGraph, - SceneGridRow, SceneObject, SceneQueryRunner, VizPanel, @@ -19,7 +18,6 @@ import { DashboardScene } from '../scene/DashboardScene'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelMenuBehavior } from '../scene/PanelMenuBehavior'; -import { RowActions } from '../scene/row-actions/RowActions'; import { dashboardSceneGraph } from './dashboardSceneGraph'; @@ -241,17 +239,6 @@ export function getDefaultVizPanel(dashboard: DashboardScene): VizPanel { }); } -export function getDefaultRow(dashboard: DashboardScene): SceneGridRow { - const id = dashboardSceneGraph.getNextPanelId(dashboard); - - return new SceneGridRow({ - key: getVizPanelKeyForPanelId(id), - title: 'Row title', - actions: new RowActions({}), - y: 0, - }); -} - export function isLibraryPanel(vizPanel: VizPanel): boolean { return getLibraryPanelBehavior(vizPanel) !== undefined; }