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
pull/93911/head
Torkel Ödegaard 10 months ago committed by GitHub
parent 7e94d05d39
commit 1941ae21d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      .betterer.results
  2. 17
      public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.test.tsx
  3. 17
      public/app/features/dashboard-scene/inspect/HelpWizard/SupportSnapshotService.test.ts
  4. 33
      public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx
  5. 16
      public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts
  6. 26
      public/app/features/dashboard-scene/saving/DashboardPrompt.test.tsx
  7. 113
      public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx
  8. 39
      public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx
  9. 180
      public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx
  10. 14
      public/app/features/dashboard-scene/scene/DashboardGridItem.test.tsx
  11. 606
      public/app/features/dashboard-scene/scene/DashboardScene.test.tsx
  12. 290
      public/app/features/dashboard-scene/scene/DashboardScene.tsx
  13. 50
      public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts
  14. 21
      public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts
  15. 7
      public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx
  16. 48
      public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx
  17. 16
      public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx
  18. 3
      public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx
  19. 35
      public/app/features/dashboard-scene/scene/ViewPanelScene.test.tsx
  20. 9
      public/app/features/dashboard-scene/scene/keyboardShortcuts.ts
  21. 281
      public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx
  22. 355
      public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx
  23. 23
      public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx
  24. 88
      public/app/features/dashboard-scene/scene/types.ts
  25. 14
      public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts
  26. 11
      public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts
  27. 6
      public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts
  28. 6
      public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts
  29. 5
      public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx
  30. 5
      public/app/features/dashboard-scene/settings/DashboardLinksEditView.test.tsx
  31. 5
      public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx
  32. 5
      public/app/features/dashboard-scene/settings/PermissionsEditView.test.tsx
  33. 28
      public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx
  34. 5
      public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx
  35. 17
      public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.test.tsx
  36. 17
      public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.test.tsx
  37. 17
      public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.test.tsx
  38. 17
      public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx
  39. 16
      public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareAlerts.test.tsx
  40. 31
      public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareExternally.test.tsx
  41. 5
      public/app/features/dashboard-scene/sharing/ShareDrawer/ShareDrawer.test.tsx
  42. 17
      public/app/features/dashboard-scene/sharing/ShareLinkTab.test.tsx
  43. 69
      public/app/features/dashboard-scene/sharing/public-dashboards/utils.ts
  44. 29
      public/app/features/dashboard-scene/solo/useSoloPanel.ts
  45. 119
      public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts
  46. 47
      public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts
  47. 251
      public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts
  48. 75
      public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts
  49. 7
      public/app/features/dashboard-scene/utils/test-utils.ts
  50. 13
      public/app/features/dashboard-scene/utils/utils.ts

@ -2785,6 +2785,10 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"] [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "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": [ "public/app/features/dashboard-scene/serialization/angularMigration.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"] [0, 0, 0, "Unexpected any. Specify a different type.", "0"]
], ],

@ -2,12 +2,12 @@ import { render, screen } from '@testing-library/react';
import { FieldType, getDefaultTimeRange, LoadingState, toDataFrame } from '@grafana/data'; import { FieldType, getDefaultTimeRange, LoadingState, toDataFrame } from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; 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 { DashboardScene } from '../../scene/DashboardScene';
import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks'; import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks';
import { panelMenuBehavior } from '../../scene/PanelMenuBehavior'; import { panelMenuBehavior } from '../../scene/PanelMenuBehavior';
import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager';
import { HelpWizard } from './HelpWizard'; import { HelpWizard } from './HelpWizard';
@ -67,18 +67,7 @@ async function buildTestScene() {
canEdit: true, canEdit: true,
isEmbedded: false, isEmbedded: false,
}, },
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([panel]),
children: [
new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: panel,
}),
],
}),
}); });
await new Promise((r) => setTimeout(r, 1)); await new Promise((r) => setTimeout(r, 1));

@ -1,10 +1,10 @@
import { FieldType, getDefaultTimeRange, LoadingState, toDataFrame } from '@grafana/data'; 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 { DashboardScene } from '../../scene/DashboardScene';
import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks'; import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks';
import { panelMenuBehavior } from '../../scene/PanelMenuBehavior'; import { panelMenuBehavior } from '../../scene/PanelMenuBehavior';
import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager';
import { SnapshotTab, SupportSnapshotService } from './SupportSnapshotService'; import { SnapshotTab, SupportSnapshotService } from './SupportSnapshotService';
@ -114,18 +114,7 @@ async function buildTestScene() {
canEdit: true, canEdit: true,
isEmbedded: false, isEmbedded: false,
}, },
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([panel]),
children: [
new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: panel,
}),
],
}),
}); });
await new Promise((r) => setTimeout(r, 1)); await new Promise((r) => setTimeout(r, 1));

@ -12,7 +12,7 @@ import {
} from '@grafana/data'; } from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { setPluginImportUtils, setRunRequest } from '@grafana/runtime'; 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 * as libpanels from 'app/features/library-panels/state/api';
import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; import { getStandardTransformers } from 'app/features/transformers/standardTransformers';
@ -20,6 +20,7 @@ import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel'; import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
import { activateFullSceneTree } from '../utils/test-utils'; import { activateFullSceneTree } from '../utils/test-utils';
import { findVizPanelByKey } from '../utils/utils'; import { findVizPanelByKey } from '../utils/utils';
@ -106,7 +107,7 @@ describe('InspectJsonTab', () => {
const { tab } = await buildTestScene(); const { tab } = await buildTestScene();
const obj = JSON.parse(tab.state.jsonText); 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); expect(tab.isEditable()).toBe(true);
}); });
@ -114,7 +115,7 @@ describe('InspectJsonTab', () => {
const { tab } = await buildTestSceneWithLibraryPanel(); const { tab } = await buildTestSceneWithLibraryPanel();
const obj = JSON.parse(tab.state.jsonText); 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(obj.type).toEqual('table');
expect(tab.isEditable()).toBe(false); expect(tab.isEditable()).toBe(false);
}); });
@ -202,18 +203,7 @@ async function buildTestScene() {
meta: { meta: {
canEdit: true, canEdit: true,
}, },
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([panel]),
children: [
new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: panel,
}),
],
}),
}); });
activateFullSceneTree(scene); activateFullSceneTree(scene);
@ -265,24 +255,13 @@ async function buildTestSceneWithLibraryPanel() {
jest.spyOn(libpanels, 'getLibraryPanel').mockResolvedValue({ ...libraryPanelState, ...panel }); 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({ const scene = new DashboardScene({
title: 'hello', title: 'hello',
uid: 'dash-1', uid: 'dash-1',
meta: { meta: {
canEdit: true, canEdit: true,
}, },
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([libraryPanel]),
children: [gridItem],
}),
}); });
activateFullSceneTree(scene); activateFullSceneTree(scene);

@ -21,6 +21,7 @@ import * as libAPI from 'app/features/library-panels/state/api';
import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel'; import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
import { activateFullSceneTree } from '../utils/test-utils'; import { activateFullSceneTree } from '../utils/test-utils';
import { findVizPanelByKey, getQueryRunnerFor } from '../utils/utils'; import { findVizPanelByKey, getQueryRunnerFor } from '../utils/utils';
@ -124,7 +125,8 @@ describe('PanelEditor', () => {
const { panelEditor, dashboard } = await setup({ isNewPanel: true }); const { panelEditor, dashboard } = await setup({ isNewPanel: true });
panelEditor.onDiscard(); 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 () => { it('should discard query runner changes', async () => {
@ -212,8 +214,10 @@ describe('PanelEditor', () => {
editPanel: editScene, editPanel: editScene,
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }), $timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
isEditing: true, isEditing: true,
body: new SceneGridLayout({ body: new DefaultGridLayoutManager({
children: [gridItem], grid: new SceneGridLayout({
children: [gridItem],
}),
}), }),
}); });
@ -332,8 +336,10 @@ async function setup(options: SetupOptions = {}) {
}), }),
], ],
}), }),
body: new SceneGridLayout({ body: new DefaultGridLayoutManager({
children: [gridItem], grid: new SceneGridLayout({
children: [gridItem],
}),
}), }),
}); });

@ -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 { ContextSrv, setContextSrv } from 'app/core/services/context_srv';
import { DashboardControls } from '../scene/DashboardControls'; import { DashboardControls } from '../scene/DashboardControls';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
import { ignoreChanges } from './DashboardPrompt'; import { ignoreChanges } from './DashboardPrompt';
@ -136,20 +136,14 @@ function buildTestScene(overrides?: Partial<DashboardSceneState>) {
}), }),
controls: new DashboardControls({}), controls: new DashboardControls({}),
$behaviors: [new behaviors.CursorSync({})], $behaviors: [new behaviors.CursorSync({})],
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([
children: [ new VizPanel({
new DashboardGridItem({ title: 'Panel A',
key: 'griditem-1', key: 'panel-1',
x: 0, pluginId: 'table',
body: new VizPanel({ $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
title: 'Panel A', }),
key: 'panel-1', ]),
pluginId: 'table',
$data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
}),
}),
],
}),
...overrides, ...overrides,
}); });

@ -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 { LibraryPanel } from '@grafana/schema/dist/esm/index.gen';
import { activateFullSceneTree } from '../utils/test-utils'; import { activateFullSceneTree } from '../utils/test-utils';
import { AddLibraryPanelDrawer } from './AddLibraryPanelDrawer'; import { AddLibraryPanelDrawer } from './AddLibraryPanelDrawer';
import { DashboardGridItem } from './DashboardGridItem';
import { DashboardScene } from './DashboardScene'; import { DashboardScene } from './DashboardScene';
import { LibraryPanelBehavior } from './LibraryPanelBehavior'; import { LibraryPanelBehavior } from './LibraryPanelBehavior';
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
jest.mock('@grafana/runtime', () => ({ jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'), ...jest.requireActual('@grafana/runtime'),
@ -41,13 +41,12 @@ describe('AddLibraryPanelWidget', () => {
addLibPanelDrawer.onAddLibraryPanel(panelInfo); addLibPanelDrawer.onAddLibraryPanel(panelInfo);
const layout = dashboard.state.body as SceneGridLayout; const panels = dashboard.state.body.getVizPanels();
const gridItem = layout.state.children[0] as DashboardGridItem; const panel = panels[0];
expect(layout.state.children.length).toBe(1); expect(panels.length).toBe(1);
expect(gridItem.state.body!).toBeInstanceOf(VizPanel); expect(panel.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior);
expect(gridItem.state.body.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior); expect(panel.state.key).toBe('panel-1');
expect(gridItem.state.body.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 () => { 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: { meta: {
canEdit: true, canEdit: true,
}, },
body: new SceneGridLayout({
children: [],
}),
overlay: drawer, overlay: drawer,
}); });
@ -86,24 +82,15 @@ describe('AddLibraryPanelWidget', () => {
drawer.onAddLibraryPanel(panelInfo); drawer.onAddLibraryPanel(panelInfo);
const layout = dashboard.state.body as SceneGridLayout; const panels = dashboard.state.body.getVizPanels();
const gridItem = layout.state.children[0] as DashboardGridItem; const panel = panels[0];
expect(layout.state.children.length).toBe(1); expect(panels.length).toBe(1);
expect(gridItem.state.body!).toBeInstanceOf(VizPanel); expect(panel.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior);
expect(gridItem.state.body.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior); expect(panel.state.key).toBe('panel-1');
expect(gridItem.state.body.state.key).toBe('panel-1');
expect(dashboard.state.isEditing).toBe(true); 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 () => { it('should replace grid item when grid item state is passed', async () => {
const libPanel = new VizPanel({ const libPanel = new VizPanel({
title: 'Panel Title', title: 'Panel Title',
@ -112,10 +99,6 @@ describe('AddLibraryPanelWidget', () => {
$behaviors: [new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: 'uid' })], $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() }); addLibPanelDrawer = new AddLibraryPanelDrawer({ panelToReplaceRef: libPanel.getRef() });
dashboard = new DashboardScene({ dashboard = new DashboardScene({
$timeRange: new SceneTimeRange({}), $timeRange: new SceneTimeRange({}),
@ -125,9 +108,7 @@ describe('AddLibraryPanelWidget', () => {
meta: { meta: {
canEdit: true, canEdit: true,
}, },
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([libPanel]),
children: [gridItem],
}),
overlay: addLibPanelDrawer, overlay: addLibPanelDrawer,
}); });
@ -143,71 +124,12 @@ describe('AddLibraryPanelWidget', () => {
addLibPanelDrawer.onAddLibraryPanel(panelInfo); addLibPanelDrawer.onAddLibraryPanel(panelInfo);
const layout = dashboard.state.body as SceneGridLayout; const panels = dashboard.state.body.getVizPanels();
gridItem = layout.state.children[0] as DashboardGridItem; expect(panels.length).toBe(1);
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 layout = dashboard.state.body as SceneGridLayout; const behavior = panels[0].state.$behaviors![0] as LibraryPanelBehavior;
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;
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(behavior).toBeInstanceOf(LibraryPanelBehavior);
expect(gridItem.state.key).toBe('grid-item-1');
expect(behavior.state.uid).toBe('new_uid'); expect(behavior.state.uid).toBe('new_uid');
expect(behavior.state.name).toBe('new_name'); expect(behavior.state.name).toBe('new_name');
}); });
@ -223,9 +145,6 @@ async function buildTestScene() {
meta: { meta: {
canEdit: true, canEdit: true,
}, },
body: new SceneGridLayout({
children: [],
}),
overlay: drawer, overlay: drawer,
}); });

@ -1,11 +1,4 @@
import { import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState, VizPanel } from '@grafana/scenes';
SceneComponentProps,
SceneGridLayout,
SceneObjectBase,
SceneObjectRef,
SceneObjectState,
VizPanel,
} from '@grafana/scenes';
import { LibraryPanel } from '@grafana/schema'; import { LibraryPanel } from '@grafana/schema';
import { Drawer } from '@grafana/ui'; import { Drawer } from '@grafana/ui';
import { t } from 'app/core/internationalization'; import { t } from 'app/core/internationalization';
@ -14,8 +7,7 @@ import {
LibraryPanelsSearchVariant, LibraryPanelsSearchVariant,
} from 'app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch'; } from 'app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getDashboardSceneFor, getDefaultVizPanel } from '../utils/utils';
import { NEW_PANEL_HEIGHT, NEW_PANEL_WIDTH, getDashboardSceneFor, getDefaultVizPanel } from '../utils/utils';
import { DashboardGridItem } from './DashboardGridItem'; import { DashboardGridItem } from './DashboardGridItem';
import { LibraryPanelBehavior } from './LibraryPanelBehavior'; import { LibraryPanelBehavior } from './LibraryPanelBehavior';
@ -31,16 +23,10 @@ export class AddLibraryPanelDrawer extends SceneObjectBase<AddLibraryPanelDrawer
public onAddLibraryPanel = (panelInfo: LibraryPanel) => { public onAddLibraryPanel = (panelInfo: LibraryPanel) => {
const dashboard = getDashboardSceneFor(this); 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); newPanel.setState({
body.setState({
$behaviors: [new LibraryPanelBehavior({ uid: panelInfo.uid, name: panelInfo.name })], $behaviors: [new LibraryPanelBehavior({ uid: panelInfo.uid, name: panelInfo.name })],
}); });
@ -53,22 +39,9 @@ export class AddLibraryPanelDrawer extends SceneObjectBase<AddLibraryPanelDrawer
throw new Error('Trying to replace a panel that does not have a DashboardGridItem'); throw new Error('Trying to replace a panel that does not have a DashboardGridItem');
} }
gridItemToReplace.setState({ body }); gridItemToReplace.setState({ body: newPanel });
} else { } else {
const newGridItem = new DashboardGridItem({ dashboard.addPanel(newPanel);
height: NEW_PANEL_HEIGHT,
width: NEW_PANEL_WIDTH,
x: 0,
y: 0,
body: body,
key: `grid-item-${panelId}`,
});
layout.setState({ children: [newGridItem, ...layout.state.children] });
if (!dashboard.state.isEditing) {
dashboard.onEnterEditMode();
}
} }
this.onClose(); this.onClose();

@ -11,16 +11,16 @@ import {
} from '@grafana/data'; } from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { setPluginImportUtils } from '@grafana/runtime'; import { setPluginImportUtils } from '@grafana/runtime';
import { SceneDataTransformer, SceneFlexLayout, SceneGridLayout, SceneQueryRunner, VizPanel } from '@grafana/scenes'; import { SceneDataTransformer, SceneFlexLayout, SceneQueryRunner, VizPanel } from '@grafana/scenes';
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types'; import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
import { activateFullSceneTree } from '../utils/test-utils'; import { activateFullSceneTree } from '../utils/test-utils';
import { DashboardDatasourceBehaviour } from './DashboardDatasourceBehaviour'; import { DashboardDatasourceBehaviour } from './DashboardDatasourceBehaviour';
import { DashboardGridItem } from './DashboardGridItem';
import { DashboardScene } from './DashboardScene'; import { DashboardScene } from './DashboardScene';
import { LibraryPanelBehavior } from './LibraryPanelBehavior'; import { LibraryPanelBehavior } from './LibraryPanelBehavior';
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
const grafanaDs = { const grafanaDs = {
id: 1, id: 1,
@ -163,18 +163,7 @@ describe('DashboardDatasourceBehaviour', () => {
meta: { meta: {
canEdit: true, canEdit: true,
}, },
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([sourcePanel]),
children: [
new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: sourcePanel,
}),
],
}),
}); });
activateFullSceneTree(scene); activateFullSceneTree(scene);
@ -183,22 +172,10 @@ describe('DashboardDatasourceBehaviour', () => {
const spy = jest.spyOn(dashboardDSPanel.state.$data as SceneQueryRunner, 'runQueries'); 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 // we add the new panel, it should run it's query as usual
layout.setState({ scene.addPanel(dashboardDSPanel);
children: [
...layout.state.children,
new DashboardGridItem({
key: 'griditem-2',
x: 0,
y: 0,
width: 10,
height: 12,
body: dashboardDSPanel,
}),
],
});
dashboardDSPanel.activate(); dashboardDSPanel.activate();
@ -237,26 +214,7 @@ describe('DashboardDatasourceBehaviour', () => {
meta: { meta: {
canEdit: true, canEdit: true,
}, },
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]),
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,
}),
],
}),
}); });
const spy = jest.spyOn(dashboardDSPanel.state.$data as SceneQueryRunner, 'runQueries'); const spy = jest.spyOn(dashboardDSPanel.state.$data as SceneQueryRunner, 'runQueries');
@ -313,26 +271,7 @@ describe('DashboardDatasourceBehaviour', () => {
meta: { meta: {
canEdit: true, canEdit: true,
}, },
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]),
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,
}),
],
}),
}); });
const sceneDeactivate = activateFullSceneTree(scene); const sceneDeactivate = activateFullSceneTree(scene);
@ -382,26 +321,7 @@ describe('DashboardDatasourceBehaviour', () => {
meta: { meta: {
canEdit: true, canEdit: true,
}, },
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, anotherPanel]),
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,
}),
],
}),
}); });
const sceneDeactivate = activateFullSceneTree(scene); const sceneDeactivate = activateFullSceneTree(scene);
@ -457,26 +377,7 @@ describe('DashboardDatasourceBehaviour', () => {
meta: { meta: {
canEdit: true, canEdit: true,
}, },
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]),
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,
}),
],
}),
}); });
try { try {
@ -526,26 +427,7 @@ describe('DashboardDatasourceBehaviour', () => {
meta: { meta: {
canEdit: true, canEdit: true,
}, },
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]),
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,
}),
],
}),
}); });
const sceneDeactivate = activateFullSceneTree(scene); const sceneDeactivate = activateFullSceneTree(scene);
@ -605,26 +487,7 @@ describe('DashboardDatasourceBehaviour', () => {
meta: { meta: {
canEdit: true, canEdit: true,
}, },
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]),
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,
}),
],
}),
}); });
activateFullSceneTree(scene); activateFullSceneTree(scene);
@ -684,26 +547,7 @@ async function buildTestScene() {
meta: { meta: {
canEdit: true, canEdit: true,
}, },
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([sourcePanel, dashboardDSPanel]),
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,
}),
],
}),
}); });
const sceneDeactivate = activateFullSceneTree(scene); const sceneDeactivate = activateFullSceneTree(scene);

@ -8,6 +8,7 @@ import { activateFullSceneTree, buildPanelRepeaterScene } from '../utils/test-ut
import { DashboardGridItem, DashboardGridItemState } from './DashboardGridItem'; import { DashboardGridItem, DashboardGridItemState } from './DashboardGridItem';
import { DashboardScene } from './DashboardScene'; import { DashboardScene } from './DashboardScene';
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
jest.mock('@grafana/runtime', () => ({ jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'), ...jest.requireActual('@grafana/runtime'),
@ -122,8 +123,8 @@ describe('PanelRepeaterGridItem', () => {
$variables: new SceneVariableSet({ $variables: new SceneVariableSet({
variables: [variable], variables: [variable],
}), }),
body: new SceneGridLayout({ body: new DefaultGridLayoutManager({
children: [panel], grid: new SceneGridLayout({ children: [panel] }),
}), }),
}); });
@ -193,8 +194,10 @@ describe('PanelRepeaterGridItem', () => {
$variables: new SceneVariableSet({ $variables: new SceneVariableSet({
variables: [variable], variables: [variable],
}), }),
body: new SceneGridLayout({ body: new DefaultGridLayoutManager({
children: [panel, panel2], grid: new SceneGridLayout({
children: [panel, panel2],
}),
}), }),
}); });
@ -269,7 +272,8 @@ describe('PanelRepeaterGridItem', () => {
const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 0, maxPerRow: 2, itemHeight: 10 }); const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 0, maxPerRow: 2, itemHeight: 10 });
const layoutForceRender = jest.fn(); 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); activateFullSceneTree(scene);

@ -25,13 +25,14 @@ import { DecoratedRevisionModel } from '../settings/VersionsEditView';
import { historySrv } from '../settings/version-history/HistorySrv'; import { historySrv } from '../settings/version-history/HistorySrv';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { djb2Hash } from '../utils/djb2Hash'; import { djb2Hash } from '../utils/djb2Hash';
import { findVizPanelByKey } from '../utils/utils'; import { findVizPanelByKey, getLibraryPanelBehavior, isLibraryPanel } from '../utils/utils';
import { DashboardControls } from './DashboardControls'; import { DashboardControls } from './DashboardControls';
import { DashboardGridItem } from './DashboardGridItem'; import { DashboardGridItem } from './DashboardGridItem';
import { DashboardScene, DashboardSceneState } from './DashboardScene'; import { DashboardScene, DashboardSceneState } from './DashboardScene';
import { LibraryPanelBehavior } from './LibraryPanelBehavior'; import { LibraryPanelBehavior } from './LibraryPanelBehavior';
import { PanelTimeRange } from './PanelTimeRange'; import { PanelTimeRange } from './PanelTimeRange';
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
import { RowActions } from './row-actions/RowActions'; import { RowActions } from './row-actions/RowActions';
jest.mock('../settings/version-history/HistorySrv'); jest.mock('../settings/version-history/HistorySrv');
@ -343,9 +344,8 @@ describe('DashboardScene', () => {
}); });
it('A change to any library panel name should set isDirty true', () => { 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 const panel = findVizPanelByKey(scene, 'panel-5')!;
.body; const behavior = getLibraryPanelBehavior(panel)!;
const behavior = libraryVizPanel.state.$behaviors![0] as LibraryPanelBehavior;
const prevValue = behavior.state.name; const prevValue = behavior.state.name;
behavior.setState({ name: 'new name' }); behavior.setState({ name: 'new name' });
@ -353,9 +353,9 @@ describe('DashboardScene', () => {
expect(scene.state.isDirty).toBe(true); expect(scene.state.isDirty).toBe(true);
scene.exitEditMode({ skipConfirm: true }); scene.exitEditMode({ skipConfirm: true });
const restoredLibraryVizPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem)
.state.body; const restoredPanel = findVizPanelByKey(scene, 'panel-5')!;
const restoredBehavior = restoredLibraryVizPanel.state.$behaviors![0] as LibraryPanelBehavior; const restoredBehavior = getLibraryPanelBehavior(restoredPanel)!;
expect(restoredBehavior.state.name).toBe(prevValue); expect(restoredBehavior.state.name).toBe(prevValue);
}); });
@ -451,160 +451,15 @@ describe('DashboardScene', () => {
expect(scene.state.isDirty).toBeFalsy(); 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', () => { it('Should create and add a new panel to the dashboard', () => {
scene.exitEditMode({ skipConfirm: true }); scene.exitEditMode({ skipConfirm: true });
expect(scene.state.isEditing).toBe(false); expect(scene.state.isEditing).toBe(false);
scene.onCreateNewPanel(); const panel = scene.onCreateNewPanel();
const body = scene.state.body as SceneGridLayout;
const gridItem = body.state.children[0] as DashboardGridItem;
expect(scene.state.isEditing).toBe(true); expect(scene.state.isEditing).toBe(true);
expect(body.state.children.length).toBe(6); expect(scene.state.body.getVizPanels().length).toBe(7);
expect(gridItem.state.body!.state.key).toBe('panel-7'); expect(panel.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);
}); });
it('Should fail to copy a panel if it does not have a grid item parent', () => { 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', () => { 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); scene.copyPanel(vizPanel as VizPanel);
expect(store.exists(LS_PANEL_COPY_KEY)).toBe(true); expect(store.exists(LS_PANEL_COPY_KEY)).toBe(true);
}); });
it('Should copy a library viz panel', () => { 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); scene.copyPanel(libVizPanel);
@ -651,7 +506,6 @@ describe('DashboardScene', () => {
it('Should paste a panel', () => { it('Should paste a panel', () => {
store.set(LS_PANEL_COPY_KEY, JSON.stringify({ key: 'panel-7' })); store.set(LS_PANEL_COPY_KEY, JSON.stringify({ key: 'panel-7' }));
jest.spyOn(JSON, 'parse').mockReturnThis();
jest.mocked(buildGridItemForPanel).mockReturnValue( jest.mocked(buildGridItemForPanel).mockReturnValue(
new DashboardGridItem({ new DashboardGridItem({
key: 'griditem-9', key: 'griditem-9',
@ -665,19 +519,15 @@ describe('DashboardScene', () => {
scene.pastePanel(); scene.pastePanel();
const body = scene.state.body as SceneGridLayout;
const gridItem = body.state.children[0] as DashboardGridItem;
expect(buildGridItemForPanel).toHaveBeenCalledTimes(1); expect(buildGridItemForPanel).toHaveBeenCalledTimes(1);
expect(body.state.children.length).toBe(6);
expect(gridItem.state.body!.state.key).toBe('panel-7'); const addedPanel = findVizPanelByKey(scene, 'panel-7')!;
expect(gridItem.state.y).toBe(0); expect(addedPanel).toBeDefined();
expect(store.exists(LS_PANEL_COPY_KEY)).toBe(false); expect(store.exists(LS_PANEL_COPY_KEY)).toBe(false);
}); });
it('Should paste a library viz panel', () => { it('Should paste a library viz panel', () => {
store.set(LS_PANEL_COPY_KEY, JSON.stringify({ key: 'panel-7' })); 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( jest.mocked(buildGridItemForPanel).mockReturnValue(
new DashboardGridItem({ new DashboardGridItem({
body: new VizPanel({ body: new VizPanel({
@ -691,198 +541,12 @@ describe('DashboardScene', () => {
scene.pastePanel(); 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(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 addedPanel = findVizPanelByKey(scene, 'panel-7')!;
expect(addedPanel).toBeDefined();
const body = scene.state.body as SceneGridLayout; expect(addedPanel.state.key).toBe('panel-7');
const gridRow = body.state.children[2] as SceneGridRow; expect(store.exists(LS_PANEL_COPY_KEY)).toBe(false);
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);
}); });
it('Should unlink a library panel', () => { it('Should unlink a library panel', () => {
@ -893,24 +557,14 @@ describe('DashboardScene', () => {
}); });
const scene = buildTestScene({ const scene = buildTestScene({
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([libPanel]),
children: [
new DashboardGridItem({
key: 'griditem-2',
body: libPanel,
}),
],
}),
}); });
scene.unlinkLibraryPanel(libPanel); expect(isLibraryPanel(libPanel)).toBe(true);
const body = scene.state.body as SceneGridLayout; scene.unlinkLibraryPanel(libPanel);
const gridItem = body.state.children[0] as DashboardGridItem;
expect(body.state.children.length).toBe(1); expect(isLibraryPanel(libPanel)).toBe(false);
expect(gridItem.state.body).toBeInstanceOf(VizPanel);
expect(gridItem.state.$behaviors).toBeUndefined();
}); });
it('Should create a library panel', () => { it('Should create a library panel', () => {
@ -925,50 +579,9 @@ describe('DashboardScene', () => {
body: vizPanel, body: vizPanel,
}); });
const grid = new SceneGridLayout({ children: [gridItem] });
const scene = buildTestScene({ const scene = buildTestScene({
body: new SceneGridLayout({ body: new DefaultGridLayoutManager({ grid }),
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],
}),
],
}),
}); });
const libPanel = { const libPanel = {
@ -978,12 +591,10 @@ describe('DashboardScene', () => {
scene.createLibraryPanel(vizPanel, libPanel as LibraryPanel); scene.createLibraryPanel(vizPanel, libPanel as LibraryPanel);
const layout = scene.state.body as SceneGridLayout; const newGridItem = grid.state.children[0] as DashboardGridItem;
const newGridItem = (layout.state.children[0] as SceneGridRow).state.children[0] as DashboardGridItem;
const behavior = newGridItem.state.body.state.$behaviors![0] as LibraryPanelBehavior; const behavior = newGridItem.state.body.state.$behaviors![0] as LibraryPanelBehavior;
expect(layout.state.children.length).toBe(1); expect(grid.state.children.length).toBe(1);
expect((layout.state.children[0] as SceneGridRow).state.children.length).toBe(1);
expect(newGridItem.state.body).toBeInstanceOf(VizPanel); expect(newGridItem.state.body).toBeInstanceOf(VizPanel);
expect(behavior.state.uid).toBe('uid'); expect(behavior.state.uid).toBe('uid');
expect(behavior.state.name).toBe('name'); expect(behavior.state.name).toBe('name');
@ -1166,6 +777,19 @@ describe('DashboardScene', () => {
describe('When coming from explore', () => { describe('When coming from explore', () => {
// When coming from Explore the first panel in a dashboard is a temporary panel // 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', () => { 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({ const scene = new DashboardScene({
title: 'hello', title: 'hello',
uid: 'dash-1', uid: 'dash-1',
@ -1176,37 +800,18 @@ describe('DashboardScene', () => {
}), }),
controls: new DashboardControls({}), controls: new DashboardControls({}),
$behaviors: [new behaviors.CursorSync({})], $behaviors: [new behaviors.CursorSync({})],
body: new SceneGridLayout({ body: layout,
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.onEnterEditMode(true); scene.onEnterEditMode(true);
expect(scene.state.isEditing).toBe(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 }); scene.exitEditMode({ skipConfirm: true });
const restoredGrid = scene.state.body as DefaultGridLayoutManager;
expect(scene.state.isEditing).toBe(false); 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<DashboardSceneState>) {
}), }),
controls: new DashboardControls({}), controls: new DashboardControls({}),
$behaviors: [new behaviors.CursorSync({}), new behaviors.LiveNowTimer({})], $behaviors: [new behaviors.CursorSync({}), new behaviors.LiveNowTimer({})],
body: new SceneGridLayout({ body: new DefaultGridLayoutManager({
children: [ grid: new SceneGridLayout({
new DashboardGridItem({ children: [
key: 'griditem-1', new DashboardGridItem({
x: 0, key: 'griditem-1',
body: new VizPanel({ x: 0,
title: 'Panel A', body: new VizPanel({
key: 'panel-1', title: 'Panel A',
pluginId: 'table', key: 'panel-1',
$timeRange: new PanelTimeRange({ pluginId: 'table',
from: 'now-12h', $timeRange: new PanelTimeRange({
to: 'now', from: 'now-12h',
timeZone: 'browser', to: 'now',
}), timeZone: 'browser',
$data: new SceneDataTransformer({ }),
transformations: [], $data: new SceneDataTransformer({
$data: new SceneQueryRunner({ transformations: [],
key: 'data-query-runner', $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
queries: [{ refId: 'A', target: 'aliasByMetric(carbon.**)' }],
}), }),
}), }),
}), }),
}), new DashboardGridItem({
new DashboardGridItem({ key: 'griditem-2',
key: 'griditem-2', body: new VizPanel({
body: new VizPanel({ title: 'Panel B',
title: 'Panel B', key: 'panel-2',
key: 'panel-2', pluginId: 'table',
pluginId: 'table', }),
}), }),
}), new SceneGridRow({
new SceneGridRow({ key: 'panel-3',
key: 'panel-3', actions: new RowActions({}),
actions: new RowActions({}), children: [
children: [ new DashboardGridItem({
new DashboardGridItem({ body: new VizPanel({
body: new VizPanel({ title: 'Panel C',
title: 'Panel C', key: 'panel-4',
key: 'panel-4', pluginId: 'table',
pluginId: 'table', }),
}), }),
}), new DashboardGridItem({
new DashboardGridItem({ body: new VizPanel({
body: new VizPanel({ title: 'Library Panel',
title: 'Library Panel', pluginId: 'table',
pluginId: 'table', key: 'panel-5',
key: 'panel-5', $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })],
$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({
new DashboardGridItem({ body: new VizPanel({
body: new VizPanel({ title: 'Library Panel',
title: 'Library Panel', pluginId: 'table',
pluginId: 'table', key: 'panel-6',
key: 'panel-6', $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })],
$behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], }),
}), }),
}), ],
], }),
}), }),
...overrides, ...overrides,
}); });

@ -12,14 +12,12 @@ import {
} from '@grafana/data'; } from '@grafana/data';
import { config, locationService } from '@grafana/runtime'; import { config, locationService } from '@grafana/runtime';
import { import {
SceneFlexLayout,
sceneGraph,
SceneGridLayout,
SceneGridRow, SceneGridRow,
SceneObject, SceneObject,
SceneObjectBase, SceneObjectBase,
SceneObjectRef, SceneObjectRef,
SceneObjectState, SceneObjectState,
SceneTimeRange,
sceneUtils, sceneUtils,
SceneVariable, SceneVariable,
SceneVariableDependencyConfigLike, SceneVariableDependencyConfigLike,
@ -52,15 +50,10 @@ import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { djb2Hash } from '../utils/djb2Hash'; import { djb2Hash } from '../utils/djb2Hash';
import { getDashboardUrl, getViewPanelUrl } from '../utils/urlBuilders'; import { getDashboardUrl, getViewPanelUrl } from '../utils/urlBuilders';
import { import {
NEW_PANEL_HEIGHT,
NEW_PANEL_WIDTH,
forceRenderChildren,
getClosestVizPanel, getClosestVizPanel,
getDashboardSceneFor, getDashboardSceneFor,
getDefaultRow,
getDefaultVizPanel, getDefaultVizPanel,
getPanelIdForVizPanel, getPanelIdForVizPanel,
getQueryRunnerFor,
getVizPanelKeyForPanelId, getVizPanelKeyForPanelId,
isPanelClone, isPanelClone,
} from '../utils/utils'; } from '../utils/utils';
@ -74,6 +67,8 @@ import { LibraryPanelBehavior } from './LibraryPanelBehavior';
import { RowRepeaterBehavior } from './RowRepeaterBehavior'; import { RowRepeaterBehavior } from './RowRepeaterBehavior';
import { ViewPanelScene } from './ViewPanelScene'; import { ViewPanelScene } from './ViewPanelScene';
import { setupKeyboardShortcuts } from './keyboardShortcuts'; 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']; export const PERSISTED_PROPS = ['title', 'description', 'tags', 'editable', 'graphTooltip', 'links', 'meta', 'preload'];
@ -95,7 +90,7 @@ export interface DashboardSceneState extends SceneObjectState {
/** @deprecated */ /** @deprecated */
id?: number | null; id?: number | null;
/** Layout of panels */ /** Layout of panels */
body: SceneObject; body: DashboardLayoutManager;
/** NavToolbar actions */ /** NavToolbar actions */
actions?: SceneObject[]; actions?: SceneObject[];
/** Fixed row at the top of the canvas with for example variables and time range controls */ /** 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<DashboardSceneState> {
title: 'Dashboard', title: 'Dashboard',
meta: {}, meta: {},
editable: true, editable: true,
body: state.body ?? new SceneFlexLayout({ children: [] }), $timeRange: state.$timeRange ?? new SceneTimeRange({}),
body: state.body ?? DefaultGridLayoutManager.fromVizPanels(),
links: state.links ?? [], links: state.links ?? [],
...state, ...state,
}); });
@ -235,7 +231,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
this.setState({ isEditing: true }); this.setState({ isEditing: true });
// Propagate change edit mode change to children // Propagate change edit mode change to children
this.propagateEditModeChange(); this.state.body.editModeChanged(true);
// Propagate edit mode to scopes // Propagate edit mode to scopes
this._scopesFacade?.enterReadOnly(); this._scopesFacade?.enterReadOnly();
@ -272,13 +268,6 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
this._changeTracker.startTrackingChanges(); 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 }) { public exitEditMode({ skipConfirm, restoreInitialState }: { skipConfirm: boolean; restoreInitialState?: boolean }) {
if (!this.canDiscard()) { if (!this.canDiscard()) {
console.error('Trying to discard back to a state that does not exist, initialState undefined'); console.error('Trying to discard back to a state that does not exist, initialState undefined');
@ -342,7 +331,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
} }
// Disable grid dragging // Disable grid dragging
this.propagateEditModeChange(); this.state.body.editModeChanged(false);
} }
private cleanupStateFromExplore() { private cleanupStateFromExplore() {
@ -352,10 +341,8 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
this._initialSaveModel.panels = this._initialSaveModel.panels.slice(1); this._initialSaveModel.panels = this._initialSaveModel.panels.slice(1);
} }
if (this._initialState && this._initialState.body instanceof SceneGridLayout) { if (this._initialState) {
this._initialState.body.setState({ this._initialState.body.cleanUpStateFromExplore?.();
children: this._initialState.body.state.children.slice(1),
});
} }
} }
@ -467,79 +454,15 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
return this._initialState; 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 { public addPanel(vizPanel: VizPanel): void {
if (!(this.state.body instanceof SceneGridLayout)) { if (!this.state.isEditing) {
throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); this.onEnterEditMode();
} }
const sceneGridLayout = this.state.body; this.state.body.addPanel(vizPanel);
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],
});
} }
public createLibraryPanel(panelToReplace: VizPanel, libPanel: LibraryPanel) { 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({ const body = panelToReplace.clone({
$behaviors: [new LibraryPanelBehavior({ uid: libPanel.uid, name: libPanel.name })], $behaviors: [new LibraryPanelBehavior({ uid: libPanel.uid, name: libPanel.name })],
}); });
@ -554,74 +477,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
} }
public duplicatePanel(vizPanel: VizPanel) { public duplicatePanel(vizPanel: VizPanel) {
if (!vizPanel.parent) { this.state.body.duplicatePanel(vizPanel);
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],
});
} }
public copyPanel(vizPanel: VizPanel) { public copyPanel(vizPanel: VizPanel) {
@ -643,83 +499,23 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
} }
public pastePanel() { 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 jsonData = store.get(LS_PANEL_COPY_KEY);
const jsonObj = JSON.parse(jsonData); const jsonObj = JSON.parse(jsonData);
const panelModel = new PanelModel(jsonObj); const panelModel = new PanelModel(jsonObj);
const gridItem = buildGridItemForPanel(panelModel); 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 panelId = dashboardSceneGraph.getNextPanelId(this);
const panel = gridItem.state.body;
gridItem.state.body.setState({ panel.setState({ key: getVizPanelKeyForPanelId(panelId) });
key: getVizPanelKeyForPanelId(panelId), panel.clearParent();
});
gridItem.setState({ this.state.body.addPanel(panel);
height: NEW_PANEL_HEIGHT,
width: NEW_PANEL_WIDTH,
x: 0,
y: 0,
key: `grid-item-${panelId}`,
});
sceneGridLayout.setState({
children: [gridItem, ...sceneGridLayout.state.children],
});
store.delete(LS_PANEL_COPY_KEY); store.delete(LS_PANEL_COPY_KEY);
} }
public removePanel(panel: VizPanel) { public removePanel(panel: VizPanel) {
const panels: SceneObject[] = []; this.state.body.removePanel(panel);
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 });
}
} }
public unlinkLibraryPanel(panel: VizPanel) { public unlinkLibraryPanel(panel: VizPanel) {
@ -775,18 +571,10 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
} }
public onCreateNewRow() { public onCreateNewRow() {
const row = getDefaultRow(this); this.state.body.addNewRow();
this.addRow(row);
return getPanelIdForVizPanel(row);
} }
public onCreateNewPanel(): VizPanel { public onCreateNewPanel(): VizPanel {
if (!this.state.isEditing) {
this.onEnterEditMode();
}
const vizPanel = getDefaultVizPanel(this); const vizPanel = getDefaultVizPanel(this);
this.addPanel(vizPanel); this.addPanel(vizPanel);
@ -852,40 +640,6 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
locationService.replace('/'); 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 => { public onSetScrollRef = (scrollElement: ScrollRefElement): void => {
this._scrollRef = scrollElement; 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 * 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; const layout = this._dashboard.state.body;
if (!(layout instanceof SceneGridLayout)) { if (!(layout instanceof DefaultGridLayoutManager)) {
return; return;
} }
for (const child of layout.state.children) { for (const child of layout.state.grid.state.children) {
if (!(child instanceof SceneGridRow) || !child.state.$behaviors) { if (!(child instanceof SceneGridRow) || !child.state.$behaviors) {
continue; continue;
} }

@ -1,10 +1,11 @@
import { AppEvents } from '@grafana/data'; 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 appEvents from 'app/core/app_events';
import { KioskMode } from 'app/types'; import { KioskMode } from 'app/types';
import { DashboardGridItem } from './DashboardGridItem'; import { DashboardGridItem } from './DashboardGridItem';
import { DashboardScene } from './DashboardScene'; import { DashboardScene } from './DashboardScene';
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
import { DashboardRepeatsProcessedEvent } from './types'; import { DashboardRepeatsProcessedEvent } from './types';
describe('DashboardSceneUrlSync', () => { describe('DashboardSceneUrlSync', () => {
@ -30,14 +31,17 @@ describe('DashboardSceneUrlSync', () => {
it('Should set UNSAFE_fitPanels when url has autofitpanels', () => { it('Should set UNSAFE_fitPanels when url has autofitpanels', () => {
const scene = buildTestScene(); const scene = buildTestScene();
scene.urlSync?.updateFromUrl({ autofitpanels: '' }); 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', () => { it('Should get the autofitpanels from the scene state', () => {
const scene = buildTestScene(); const scene = buildTestScene();
expect(scene.urlSync?.getUrlState().autofitpanels).toBeUndefined(); 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'); expect(scene.urlSync?.getUrlState().autofitpanels).toBe('true');
}); });
@ -89,8 +93,9 @@ describe('DashboardSceneUrlSync', () => {
expect(errorNotice).toBe(0); expect(errorNotice).toBe(0);
// fake adding clone panel // fake adding clone panel
const layout = scene.state.body as SceneGridLayout; const layout = scene.state.body as DefaultGridLayoutManager;
layout.setState({
layout.state.grid.setState({
children: [ children: [
new DashboardGridItem({ new DashboardGridItem({
key: 'griditem-1', key: 'griditem-1',
@ -114,27 +119,20 @@ function buildTestScene() {
const scene = new DashboardScene({ const scene = new DashboardScene({
title: 'hello', title: 'hello',
uid: 'dash-1', uid: 'dash-1',
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([
children: [ new VizPanel({
new DashboardGridItem({ title: 'Panel A',
key: 'griditem-1', key: 'panel-1',
x: 0, pluginId: 'table',
body: new VizPanel({ $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
title: 'Panel A', }),
key: 'panel-1',
pluginId: 'table', new VizPanel({
$data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), title: 'Panel B',
}), key: 'panel-2',
}), pluginId: 'table',
new DashboardGridItem({ }),
body: new VizPanel({ ]),
title: 'Panel B',
key: 'panel-2',
pluginId: 'table',
}),
}),
],
}),
}); });
return scene; return scene;

@ -2,7 +2,7 @@ import { Unsubscribable } from 'rxjs';
import { AppEvents } from '@grafana/data'; import { AppEvents } from '@grafana/data';
import { config, locationService } from '@grafana/runtime'; 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 appEvents from 'app/core/app_events';
import { KioskMode } from 'app/types'; import { KioskMode } from 'app/types';
@ -16,6 +16,7 @@ import { findVizPanelByKey, getLibraryPanelBehavior, isPanelClone } from '../uti
import { DashboardScene, DashboardSceneState } from './DashboardScene'; import { DashboardScene, DashboardSceneState } from './DashboardScene';
import { LibraryPanelBehavior } from './LibraryPanelBehavior'; import { LibraryPanelBehavior } from './LibraryPanelBehavior';
import { ViewPanelScene } from './ViewPanelScene'; import { ViewPanelScene } from './ViewPanelScene';
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
import { DashboardRepeatsProcessedEvent } from './types'; import { DashboardRepeatsProcessedEvent } from './types';
export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
@ -29,9 +30,10 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
getUrlState(): SceneObjectUrlValues { getUrlState(): SceneObjectUrlValues {
const state = this._scene.state; const state = this._scene.state;
return { return {
inspect: state.inspectPanelKey, inspect: state.inspectPanelKey,
autofitpanels: state.body instanceof SceneGridLayout && !!state.body.state.UNSAFE_fitPanels ? 'true' : undefined, autofitpanels: this.getAutoFitPanels(),
viewPanel: state.viewPanelScene?.getUrlKey(), viewPanel: state.viewPanelScene?.getUrlKey(),
editview: state.editview?.getUrlKey(), editview: state.editview?.getUrlKey(),
editPanel: state.editPanel?.getUrlKey() || undefined, 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 { updateFromUrl(values: SceneObjectUrlValues): void {
const { inspectPanelKey, viewPanelScene, isEditing, editPanel, shareView } = this._scene.state; const { inspectPanelKey, viewPanelScene, isEditing, editPanel, shareView } = this._scene.state;
const update: Partial<DashboardSceneState> = {}; const update: Partial<DashboardSceneState> = {};
@ -142,11 +152,12 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
update.shareView = undefined; 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'; const UNSAFE_fitPanels = typeof values.autofitpanels === 'string';
if (!!this._scene.state.body.state.UNSAFE_fitPanels !== UNSAFE_fitPanels) { if (!!layout.state.grid.state.UNSAFE_fitPanels !== UNSAFE_fitPanels) {
this._scene.state.body.setState({ UNSAFE_fitPanels }); layout.state.grid.setState({ UNSAFE_fitPanels });
} }
} }

@ -13,6 +13,7 @@ import { activateFullSceneTree } from '../utils/test-utils';
import { DashboardGridItem } from './DashboardGridItem'; import { DashboardGridItem } from './DashboardGridItem';
import { DashboardScene } from './DashboardScene'; import { DashboardScene } from './DashboardScene';
import { LibraryPanelBehavior } from './LibraryPanelBehavior'; import { LibraryPanelBehavior } from './LibraryPanelBehavior';
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
setPluginImportUtils({ setPluginImportUtils({
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})), importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
@ -170,8 +171,10 @@ async function buildTestSceneWithLibraryPanel() {
meta: { meta: {
canEdit: true, canEdit: true,
}, },
body: new SceneGridLayout({ body: new DefaultGridLayoutManager({
children: [gridItem], grid: new SceneGridLayout({
children: [gridItem],
}),
}), }),
}); });

@ -5,15 +5,15 @@ import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { LocationServiceProvider, config, locationService } from '@grafana/runtime'; 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 { playlistSrv } from 'app/features/playlist/PlaylistSrv';
import { DashboardMeta } from 'app/types'; import { DashboardMeta } from 'app/types';
import { buildPanelEditScene } from '../panel-edit/PanelEditor'; import { buildPanelEditScene } from '../panel-edit/PanelEditor';
import { DashboardGridItem } from './DashboardGridItem';
import { DashboardScene } from './DashboardScene'; import { DashboardScene } from './DashboardScene';
import { ToolbarActions } from './NavToolbarActions'; import { ToolbarActions } from './NavToolbarActions';
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
jest.mock('app/features/playlist/PlaylistSrv', () => ({ jest.mock('app/features/playlist/PlaylistSrv', () => ({
playlistSrv: { playlistSrv: {
@ -120,9 +120,8 @@ describe('NavToolbarActions', () => {
await act(() => { await act(() => {
dashboard.onEnterEditMode(); dashboard.onEnterEditMode();
const editingPanel = ((dashboard.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state const panel = dashboard.state.body.getVizPanels()[0];
.body as VizPanel; dashboard.setState({ editPanel: buildPanelEditScene(panel, true) });
dashboard.setState({ editPanel: buildPanelEditScene(editingPanel, true) });
}); });
expect(await screen.findByText('Save dashboard')).toBeInTheDocument(); expect(await screen.findByText('Save dashboard')).toBeInTheDocument();
@ -135,9 +134,8 @@ describe('NavToolbarActions', () => {
await act(() => { await act(() => {
dashboard.onEnterEditMode(); dashboard.onEnterEditMode();
const editingPanel = ((dashboard.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state const panel = dashboard.state.body.getVizPanels()[0];
.body as VizPanel; dashboard.setState({ editPanel: buildPanelEditScene(panel) });
dashboard.setState({ editPanel: buildPanelEditScene(editingPanel) });
}); });
expect(await screen.findByText('Save dashboard')).toBeInTheDocument(); expect(await screen.findByText('Save dashboard')).toBeInTheDocument();
@ -207,27 +205,19 @@ function setup(meta?: DashboardMeta) {
}, },
title: 'hello', title: 'hello',
uid: 'dash-1', uid: 'dash-1',
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([
children: [ new VizPanel({
new DashboardGridItem({ title: 'Panel A',
key: 'griditem-1', key: 'panel-1',
x: 0, pluginId: 'table',
body: new VizPanel({ $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
title: 'Panel A', }),
key: 'panel-1', new VizPanel({
pluginId: 'table', title: 'Panel B',
$data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), key: 'panel-2',
}), pluginId: 'table',
}), }),
new DashboardGridItem({ ]),
body: new VizPanel({
title: 'Panel B',
key: 'panel-2',
pluginId: 'table',
}),
}),
],
}),
}); });
const context = getGrafanaContextMock(); const context = getGrafanaContextMock();

@ -11,7 +11,6 @@ import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { getPluginLinkExtensions, locationService } from '@grafana/runtime'; import { getPluginLinkExtensions, locationService } from '@grafana/runtime';
import { import {
LocalValueVariable, LocalValueVariable,
SceneGridLayout,
SceneQueryRunner, SceneQueryRunner,
SceneTimeRange, SceneTimeRange,
SceneVariableSet, SceneVariableSet,
@ -23,10 +22,10 @@ import { GetExploreUrlArguments } from 'app/core/utils/explore';
import { buildPanelEditScene } from '../panel-edit/PanelEditor'; import { buildPanelEditScene } from '../panel-edit/PanelEditor';
import { DashboardGridItem } from './DashboardGridItem';
import { DashboardScene } from './DashboardScene'; import { DashboardScene } from './DashboardScene';
import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks'; import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks';
import { panelMenuBehavior } from './PanelMenuBehavior'; import { panelMenuBehavior } from './PanelMenuBehavior';
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
const mocks = { const mocks = {
contextSrv: jest.mocked(contextSrv), contextSrv: jest.mocked(contextSrv),
@ -588,18 +587,7 @@ async function buildTestScene(options: SceneOptions) {
canEdit: true, canEdit: true,
isEmbedded: options.isEmbedded ?? false, isEmbedded: options.isEmbedded ?? false,
}, },
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([panel]),
children: [
new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: panel,
}),
],
}),
}); });
await new Promise((r) => setTimeout(r, 1)); await new Promise((r) => setTimeout(r, 1));

@ -21,6 +21,7 @@ import { DashboardGridItem, RepeatDirection } from './DashboardGridItem';
import { DashboardScene } from './DashboardScene'; import { DashboardScene } from './DashboardScene';
import { panelMenuBehavior, repeatPanelMenuBehavior } from './PanelMenuBehavior'; import { panelMenuBehavior, repeatPanelMenuBehavior } from './PanelMenuBehavior';
import { RowRepeaterBehavior } from './RowRepeaterBehavior'; import { RowRepeaterBehavior } from './RowRepeaterBehavior';
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
import { RowActions } from './row-actions/RowActions'; import { RowActions } from './row-actions/RowActions';
jest.mock('@grafana/runtime', () => ({ jest.mock('@grafana/runtime', () => ({
@ -350,7 +351,7 @@ function buildScene(
}), }),
], ],
}), }),
body: grid, body: new DefaultGridLayoutManager({ grid }),
}); });
const rowToRepeat = repeatBehavior.parent as SceneGridRow; const rowToRepeat = repeatBehavior.parent as SceneGridRow;

@ -3,6 +3,7 @@ import { LocalValueVariable, SceneGridLayout, SceneGridRow, SceneVariableSet, Vi
import { DashboardGridItem } from './DashboardGridItem'; import { DashboardGridItem } from './DashboardGridItem';
import { DashboardScene } from './DashboardScene'; import { DashboardScene } from './DashboardScene';
import { ViewPanelScene } from './ViewPanelScene'; import { ViewPanelScene } from './ViewPanelScene';
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
jest.mock('@grafana/runtime', () => ({ jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'), ...jest.requireActual('@grafana/runtime'),
@ -32,23 +33,25 @@ function buildScene(options?: SceneOptions) {
}); });
const dashboard = new DashboardScene({ const dashboard = new DashboardScene({
body: new SceneGridLayout({ body: new DefaultGridLayoutManager({
children: [ grid: new SceneGridLayout({
new SceneGridRow({ children: [
x: 0, new SceneGridRow({
y: 10, x: 0,
width: 24, y: 10,
$variables: new SceneVariableSet({ width: 24,
variables: [new LocalValueVariable({ value: 'row-var-value' })], $variables: new SceneVariableSet({
}), variables: [new LocalValueVariable({ value: 'row-var-value' })],
height: 1,
children: [
new DashboardGridItem({
body: panel,
}), }),
], height: 1,
}), children: [
], new DashboardGridItem({
body: panel,
}),
],
}),
],
}),
}), }),
}); });

@ -14,6 +14,7 @@ import { getPanelIdForVizPanel } from '../utils/utils';
import { DashboardScene } from './DashboardScene'; import { DashboardScene } from './DashboardScene';
import { onRemovePanel, toggleVizPanelLegend } from './PanelMenuBehavior'; import { onRemovePanel, toggleVizPanelLegend } from './PanelMenuBehavior';
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
export function setupKeyboardShortcuts(scene: DashboardScene) { export function setupKeyboardShortcuts(scene: DashboardScene) {
const keybindings = new KeybindingSet(); const keybindings = new KeybindingSet();
@ -214,7 +215,9 @@ export function setupKeyboardShortcuts(scene: DashboardScene) {
keybindings.addBinding({ keybindings.addBinding({
key: 'd shift+c', key: 'd shift+c',
onTrigger: () => { onTrigger: () => {
scene.collapseAllRows(); if (scene.state.body instanceof DefaultGridLayoutManager) {
scene.state.body.collapseAllRows();
}
}, },
}); });
@ -222,7 +225,9 @@ export function setupKeyboardShortcuts(scene: DashboardScene) {
keybindings.addBinding({ keybindings.addBinding({
key: 'd shift+e', key: 'd shift+e',
onTrigger: () => { onTrigger: () => {
scene.expandAllRows(); if (scene.state.body instanceof DefaultGridLayoutManager) {
scene.state.body.expandAllRows();
}
}, },
}); });
} }

@ -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 };
}

@ -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<DefaultGridLayoutManagerState>
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<DefaultGridLayoutManager>) => {
return <model.state.grid.Component model={model.state.grid} />;
};
}

@ -1,7 +1,14 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; 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 { Icon, TextLink, useStyles2 } from '@grafana/ui';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
@ -11,6 +18,7 @@ import { getDashboardSceneFor, getQueryRunnerFor } from '../../utils/utils';
import { DashboardGridItem } from '../DashboardGridItem'; import { DashboardGridItem } from '../DashboardGridItem';
import { DashboardScene } from '../DashboardScene'; import { DashboardScene } from '../DashboardScene';
import { RowRepeaterBehavior } from '../RowRepeaterBehavior'; import { RowRepeaterBehavior } from '../RowRepeaterBehavior';
import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager';
import { RowOptionsButton } from './RowOptionsButton'; import { RowOptionsButton } from './RowOptionsButton';
@ -29,6 +37,11 @@ export class RowActions extends SceneObjectBase<RowActionsState> {
return getDashboardSceneFor(this); 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 => { public onUpdate = (title: string, repeat?: string | null): void => {
const row = this.getParent(); const row = this.getParent();
let repeatBehavior: RowRepeaterBehavior | undefined; let repeatBehavior: RowRepeaterBehavior | undefined;
@ -60,12 +73,8 @@ export class RowActions extends SceneObjectBase<RowActionsState> {
text: 'Are you sure you want to remove this row and all its panels?', text: 'Are you sure you want to remove this row and all its panels?',
altActionText: 'Delete row only', altActionText: 'Delete row only',
icon: 'trash-alt', icon: 'trash-alt',
onConfirm: () => { onConfirm: () => this.removeRow(true),
this.getDashboard().removeRow(this.getParent(), true); onAltAction: () => this.removeRow(),
},
onAltAction: () => {
this.getDashboard().removeRow(this.getParent());
},
}) })
); );
}; };

@ -1,5 +1,89 @@
import { BusEventWithPayload } from '@grafana/data'; import { BusEventWithPayload, RegistryItem } from '@grafana/data';
import { SceneObject } from '@grafana/scenes'; 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<T> {
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 { export interface DashboardRepeatsProcessedEventPayload {
source: SceneObject; source: SceneObject;

@ -31,6 +31,7 @@ import { DashboardGridItem } from '../scene/DashboardGridItem';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { PanelTimeRange } from '../scene/PanelTimeRange'; import { PanelTimeRange } from '../scene/PanelTimeRange';
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { NEW_LINK } from '../settings/links/utils'; import { NEW_LINK } from '../settings/links/utils';
import { getQueryRunnerFor } from '../utils/utils'; import { getQueryRunnerFor } from '../utils/utils';
@ -323,7 +324,8 @@ describe('transformSaveModelToScene', () => {
const oldModel = new DashboardModel(dashboard); const oldModel = new DashboardModel(dashboard);
const scene = createDashboardSceneFromDashboardModel(oldModel, 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); expect(body.state.children).toHaveLength(3);
const rowScene1 = body.state.children[0] as SceneGridRow; const rowScene1 = body.state.children[0] as SceneGridRow;
@ -375,7 +377,8 @@ describe('transformSaveModelToScene', () => {
const oldModel = new DashboardModel(dashboard); const oldModel = new DashboardModel(dashboard);
const scene = createDashboardSceneFromDashboardModel(oldModel, 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); expect(body.state.children).toHaveLength(1);
const rowScene = body.state.children[0] as SceneGridRow; const rowScene = body.state.children[0] as SceneGridRow;
@ -471,7 +474,8 @@ describe('transformSaveModelToScene', () => {
const oldModel = new DashboardModel(dashboard); const oldModel = new DashboardModel(dashboard);
const scene = createDashboardSceneFromDashboardModel(oldModel, 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.state.children).toHaveLength(4);
expect(body).toBeInstanceOf(SceneGridLayout); expect(body).toBeInstanceOf(SceneGridLayout);
@ -750,7 +754,9 @@ describe('transformSaveModelToScene', () => {
dashboard: repeatingRowsAndPanelsDashboardJson as DashboardDataDTO, dashboard: repeatingRowsAndPanelsDashboardJson as DashboardDataDTO,
meta: {}, 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; const row2 = body.state.children[1] as SceneGridRow;
expect(row2.state.$behaviors?.[0]).toBeInstanceOf(RowRepeaterBehavior); expect(row2.state.$behaviors?.[0]).toBeInstanceOf(RowRepeaterBehavior);

@ -37,6 +37,7 @@ import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavio
import { PanelNotices } from '../scene/PanelNotices'; import { PanelNotices } from '../scene/PanelNotices';
import { PanelTimeRange } from '../scene/PanelTimeRange'; import { PanelTimeRange } from '../scene/PanelTimeRange';
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { RowActions } from '../scene/row-actions/RowActions'; import { RowActions } from '../scene/row-actions/RowActions';
import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; import { setDashboardPanelContext } from '../scene/setDashboardPanelContext';
import { createPanelDataProvider } from '../utils/createPanelDataProvider'; import { createPanelDataProvider } from '../utils/createPanelDataProvider';
@ -221,10 +222,12 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
title: oldModel.title, title: oldModel.title,
uid: oldModel.uid, uid: oldModel.uid,
version: oldModel.version, version: oldModel.version,
body: new SceneGridLayout({ body: new DefaultGridLayoutManager({
isLazy: dto.preload ? false : true, grid: new SceneGridLayout({
children: createSceneObjectsForPanels(oldModel.panels), isLazy: dto.preload ? false : true,
$behaviors: [trackIfEmpty], children: createSceneObjectsForPanels(oldModel.panels),
$behaviors: [trackIfEmpty],
}),
}), }),
$timeRange: new SceneTimeRange({ $timeRange: new SceneTimeRange({
from: oldModel.time.from, from: oldModel.time.from,

@ -15,7 +15,7 @@ import {
} from '@grafana/data'; } from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { getPluginLinkExtensions, setPluginImportUtils } from '@grafana/runtime'; 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 { Dashboard, LoadingState, Panel, RowPanel, VariableRefresh } from '@grafana/schema';
import { PanelModel } from 'app/features/dashboard/state'; import { PanelModel } from 'app/features/dashboard/state';
import { getTimeRange } from 'app/features/dashboard/utils/timeRange'; import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
@ -27,6 +27,7 @@ import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardGridItem } from '../scene/DashboardGridItem';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { NEW_LINK } from '../settings/links/utils'; import { NEW_LINK } from '../settings/links/utils';
import { activateFullSceneTree, buildPanelRepeaterScene } from '../utils/test-utils'; import { activateFullSceneTree, buildPanelRepeaterScene } from '../utils/test-utils';
import { getVizPanelKeyForPanelId } from '../utils/utils'; import { getVizPanelKeyForPanelId } from '../utils/utils';
@ -225,7 +226,8 @@ describe('transformSceneToSaveModel', () => {
const variable = scene.state.$variables?.state.variables[0] as MultiValueVariable; const variable = scene.state.$variables?.state.variables[0] as MultiValueVariable;
variable.changeValueTo(['a', 'b', 'c']); 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 rowWithRepeat = grid.state.children[1] as SceneGridRow;
const rowRepeater = rowWithRepeat.state.$behaviors![0] as RowRepeaterBehavior; const rowRepeater = rowWithRepeat.state.$behaviors![0] as RowRepeaterBehavior;

@ -4,7 +4,6 @@ import { isEmptyObject, ScopedVars, TimeRange } from '@grafana/data';
import { import {
behaviors, behaviors,
SceneGridItemLike, SceneGridItemLike,
SceneGridLayout,
SceneGridRow, SceneGridRow,
VizPanel, VizPanel,
SceneDataTransformer, SceneDataTransformer,
@ -36,6 +35,7 @@ import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { PanelTimeRange } from '../scene/PanelTimeRange'; import { PanelTimeRange } from '../scene/PanelTimeRange';
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getLibraryPanelBehavior, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils'; import { getLibraryPanelBehavior, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils';
@ -53,8 +53,8 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
let panels: Panel[] = []; let panels: Panel[] = [];
let variables: VariableModel[] = []; let variables: VariableModel[] = [];
if (body instanceof SceneGridLayout) { if (body instanceof DefaultGridLayoutManager) {
for (const child of body.state.children) { for (const child of body.state.grid.state.children) {
if (child instanceof DashboardGridItem) { if (child instanceof DashboardGridItem) {
// handle panel repeater scenario // handle panel repeater scenario
if (child.state.variableName) { if (child.state.variableName) {

@ -11,7 +11,7 @@ import {
LoadingState, LoadingState,
PanelData, PanelData,
} from '@grafana/data'; } from '@grafana/data';
import { SceneGridLayout, SceneTimeRange, dataLayers } from '@grafana/scenes'; import { SceneTimeRange, dataLayers } from '@grafana/scenes';
import { DataSourceRef } from '@grafana/schema'; import { DataSourceRef } from '@grafana/schema';
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
@ -221,9 +221,6 @@ async function buildTestScene() {
}), }),
], ],
}), }),
body: new SceneGridLayout({
children: [],
}),
editview: annotationsView, editview: annotationsView,
}); });

@ -2,7 +2,7 @@ import { render as RTLRender } from '@testing-library/react';
import * as React from 'react'; import * as React from 'react';
import { TestProvider } from 'test/helpers/TestProvider'; 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 { DashboardCursorSync } from '@grafana/schema';
import { DashboardControls } from '../scene/DashboardControls'; import { DashboardControls } from '../scene/DashboardControls';
@ -212,9 +212,6 @@ async function buildTestScene() {
meta: { meta: {
canEdit: true, canEdit: true,
}, },
body: new SceneGridLayout({
children: [],
}),
editview: settings, editview: settings,
}); });

@ -1,4 +1,4 @@
import { behaviors, SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; import { behaviors, SceneTimeRange } from '@grafana/scenes';
import { DashboardCursorSync } from '@grafana/schema'; import { DashboardCursorSync } from '@grafana/schema';
import * as utils from '../pages/utils'; import * as utils from '../pages/utils';
@ -125,9 +125,6 @@ async function buildTestScene() {
meta: { meta: {
canEdit: true, canEdit: true,
}, },
body: new SceneGridLayout({
children: [],
}),
editview: settings, editview: settings,
}); });

@ -1,4 +1,4 @@
import { SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; import { SceneTimeRange } from '@grafana/scenes';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { activateFullSceneTree } from '../utils/test-utils'; import { activateFullSceneTree } from '../utils/test-utils';
@ -36,9 +36,6 @@ async function buildTestScene() {
meta: { meta: {
canEdit: true, canEdit: true,
}, },
body: new SceneGridLayout({
children: [],
}),
editview: permissionsView, editview: permissionsView,
}); });

@ -13,7 +13,6 @@ import { setPluginImportUtils, setRunRequest } from '@grafana/runtime';
import { import {
SceneVariableSet, SceneVariableSet,
CustomVariable, CustomVariable,
SceneGridLayout,
VizPanel, VizPanel,
AdHocFiltersVariable, AdHocFiltersVariable,
SceneVariableState, SceneVariableState,
@ -22,8 +21,8 @@ import {
import { mockDataSource } from 'app/features/alerting/unified/mocks'; import { mockDataSource } from 'app/features/alerting/unified/mocks';
import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor'; import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { activateFullSceneTree } from '../utils/test-utils'; import { activateFullSceneTree } from '../utils/test-utils';
import { VariablesEditView } from './VariablesEditView'; 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 () => { 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 // Uses function to avoid store reference to previous existing variables
const getSourceVariable = () => variableView.getVariables()[0] as CustomVariable; const getSourceVariable = () => variableView.getVariables()[0] as CustomVariable;
const getDependantPanel = () => const getDependantPanel = () => dashboard.state.body.getVizPanels()[0];
((dashboard.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state.body as VizPanel;
expect(getSourceVariable().getValue()).toBe('test'); expect(getSourceVariable().getValue()).toBe('test');
// Using description to get the interpolated value // Using description to get the interpolated value
@ -342,20 +340,14 @@ async function buildTestScene() {
}), }),
], ],
}), }),
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([
children: [ new VizPanel({
new DashboardGridItem({ title: 'Panel A',
key: 'griditem-1', description: 'Panel A depends on customVar with current value $customVar',
x: 0, key: 'panel-1',
body: new VizPanel({ pluginId: 'table',
title: 'Panel A', }),
description: 'Panel A depends on customVar with current value $customVar', ]),
key: 'panel-1',
pluginId: 'table',
}),
}),
],
}),
editview: variableView, editview: variableView,
}); });

@ -1,4 +1,4 @@
import { SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; import { SceneTimeRange } from '@grafana/scenes';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { activateFullSceneTree } from '../utils/test-utils'; import { activateFullSceneTree } from '../utils/test-utils';
@ -162,9 +162,6 @@ async function buildTestScene() {
meta: { meta: {
canEdit: true, canEdit: true,
}, },
body: new SceneGridLayout({
children: [],
}),
editview: versionsView, editview: versionsView,
}); });

@ -2,10 +2,10 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; 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 { DashboardScene } from '../../scene/DashboardScene';
import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager';
import ExportButton from './ExportButton'; import ExportButton from './ExportButton';
@ -38,18 +38,7 @@ function setup() {
title: 'hello', title: 'hello',
uid: 'dash-1', uid: 'dash-1',
$timeRange: new SceneTimeRange({}), $timeRange: new SceneTimeRange({}),
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([panel]),
children: [
new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: panel,
}),
],
}),
}); });
render(<ExportButton dashboard={dashboard} />); render(<ExportButton dashboard={dashboard} />);

@ -1,10 +1,10 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; 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 { DashboardScene } from '../../scene/DashboardScene';
import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager';
import ExportMenu from './ExportMenu'; import ExportMenu from './ExportMenu';
@ -27,18 +27,7 @@ function setup() {
title: 'hello', title: 'hello',
uid: 'dash-1', uid: 'dash-1',
$timeRange: new SceneTimeRange({}), $timeRange: new SceneTimeRange({}),
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([panel]),
children: [
new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: panel,
}),
],
}),
}); });
render(<ExportMenu dashboard={dashboard} />); render(<ExportMenu dashboard={dashboard} />);
} }

@ -2,10 +2,10 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; 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 { DashboardScene } from '../../scene/DashboardScene';
import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager';
import ShareButton from './ShareButton'; import ShareButton from './ShareButton';
@ -44,18 +44,7 @@ function setup() {
title: 'hello', title: 'hello',
uid: 'dash-1', uid: 'dash-1',
$timeRange: new SceneTimeRange({}), $timeRange: new SceneTimeRange({}),
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([panel]),
children: [
new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: panel,
}),
],
}),
}); });
render(<ShareButton dashboard={dashboard} />); render(<ShareButton dashboard={dashboard} />);

@ -1,12 +1,12 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; 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 { contextSrv } from 'app/core/services/context_srv';
import { config } from '../../../../core/config'; import { config } from '../../../../core/config';
import { DashboardGridItem } from '../../scene/DashboardGridItem';
import { DashboardScene, DashboardSceneState } from '../../scene/DashboardScene'; import { DashboardScene, DashboardSceneState } from '../../scene/DashboardScene';
import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager';
import ShareMenu from './ShareMenu'; import ShareMenu from './ShareMenu';
@ -87,18 +87,7 @@ function setup(overrides?: Partial<DashboardSceneState>) {
title: 'hello', title: 'hello',
uid: 'dash-1', uid: 'dash-1',
$timeRange: new SceneTimeRange({}), $timeRange: new SceneTimeRange({}),
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([panel]),
children: [
new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: panel,
}),
],
}),
...overrides, ...overrides,
}); });

@ -4,7 +4,6 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { import {
CustomVariable, CustomVariable,
SceneDataTransformer, SceneDataTransformer,
SceneGridLayout,
SceneQueryRunner, SceneQueryRunner,
SceneTimeRange, SceneTimeRange,
SceneVariableSet, SceneVariableSet,
@ -12,8 +11,8 @@ import {
VizPanelState, VizPanelState,
} from '@grafana/scenes'; } from '@grafana/scenes';
import { contextSrv } from 'app/core/core'; 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 { 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'; import { ShareDrawerContext } from '../../ShareDrawer/ShareDrawerContext';
@ -91,18 +90,7 @@ async function setup(panelState?: Partial<VizPanelState>, dashboardState?: Parti
title: 'hello', title: 'hello',
uid: 'dash-1', uid: 'dash-1',
$timeRange: new SceneTimeRange({}), $timeRange: new SceneTimeRange({}),
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([panel]),
children: [
new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: panel,
}),
],
}),
...dashboardState, ...dashboardState,
}); });

@ -7,18 +7,17 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { config, setPluginImportUtils } from '@grafana/runtime'; import { config, setPluginImportUtils } from '@grafana/runtime';
import { import {
CustomVariable, CustomVariable,
SceneGridLayout,
SceneQueryRunner, SceneQueryRunner,
SceneTimeRange, SceneTimeRange,
SceneVariableSet, SceneVariableSet,
VizPanel, VizPanel,
VizPanelState, VizPanelState,
} from '@grafana/scenes'; } 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 { contextSrv } from '../../../../../core/services/context_srv';
import * as sharePublicDashboardUtils from '../../../../dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; 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 { DashboardScene, DashboardSceneState } from '../../../scene/DashboardScene';
import { activateFullSceneTree } from '../../../utils/test-utils'; import { activateFullSceneTree } from '../../../utils/test-utils';
import { ShareDrawer } from '../../ShareDrawer/ShareDrawer'; import { ShareDrawer } from '../../ShareDrawer/ShareDrawer';
@ -67,6 +66,7 @@ describe('Alerts', () => {
}); });
expect(screen.queryByTestId(selectors.TemplateVariablesWarningAlert)).toBeInTheDocument(); expect(screen.queryByTestId(selectors.TemplateVariablesWarningAlert)).toBeInTheDocument();
}); });
it('when dashboard has unsupported datasources, warning is shown', async () => { it('when dashboard has unsupported datasources, warning is shown', async () => {
await buildAndRenderScenario({ await buildAndRenderScenario({
panelOverrides: { panelOverrides: {
@ -101,23 +101,14 @@ async function buildAndRenderScenario({
canEdit: true, canEdit: true,
}, },
$timeRange: new SceneTimeRange({}), $timeRange: new SceneTimeRange({}),
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([
children: [ new VizPanel({
new DashboardGridItem({ title: 'Panel A',
key: 'griditem-1', pluginId: 'table',
x: 0, key: 'panel-12',
y: 0, ...panelOverrides,
width: 10, }),
height: 12, ]),
body: new VizPanel({
title: 'Panel A',
pluginId: 'table',
key: 'panel-12',
...panelOverrides,
}),
}),
],
}),
overlay: drawer, overlay: drawer,
...overrides, ...overrides,
}); });

@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { locationService, setPluginImportUtils } from '@grafana/runtime'; 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 { render } from '../../../../../test/test-utils';
import { shareDashboardType } from '../../../dashboard/components/ShareModal/utils'; import { shareDashboardType } from '../../../dashboard/components/ShareModal/utils';
@ -63,9 +63,6 @@ async function buildAndRenderScenario() {
canEdit: true, canEdit: true,
}, },
$timeRange: new SceneTimeRange({}), $timeRange: new SceneTimeRange({}),
body: new SceneGridLayout({
children: [],
}),
overlay: drawer, overlay: drawer,
}); });

@ -6,10 +6,10 @@ import { dateTime } from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { config, locationService, setPluginImportUtils } from '@grafana/runtime'; 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 { DashboardScene } from '../scene/DashboardScene';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { activateFullSceneTree } from '../utils/test-utils'; import { activateFullSceneTree } from '../utils/test-utils';
import { ShareLinkTab } from './ShareLinkTab'; import { ShareLinkTab } from './ShareLinkTab';
@ -111,18 +111,7 @@ function buildAndRenderScenario(options: ScenarioOptions) {
canEdit: true, canEdit: true,
}, },
$timeRange: new SceneTimeRange({}), $timeRange: new SceneTimeRange({}),
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([panel]),
children: [
new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: panel,
}),
],
}),
overlay: tab, overlay: tab,
}); });

@ -1,17 +1,10 @@
import { DataSourceWithBackend } from '@grafana/runtime'; import { DataSourceWithBackend } from '@grafana/runtime';
import { import { VizPanel } from '@grafana/scenes';
SceneGridItemLike,
VizPanel,
SceneQueryRunner,
SceneDataTransformer,
SceneGridLayout,
SceneGridRow,
} from '@grafana/scenes';
import { supportedDatasources } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SupportedPubdashDatasources'; import { supportedDatasources } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SupportedPubdashDatasources';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { DashboardGridItem } from '../../scene/DashboardGridItem';
import { DashboardScene } from '../../scene/DashboardScene'; import { DashboardScene } from '../../scene/DashboardScene';
import { getQueryRunnerFor } from '../../utils/utils';
export const getUnsupportedDashboardDatasources = async (types: string[]): Promise<string[]> => { export const getUnsupportedDashboardDatasources = async (types: string[]): Promise<string[]> => {
let unsupportedDS = new Set<string>(); let unsupportedDS = new Set<string>();
@ -33,63 +26,25 @@ export const getUnsupportedDashboardDatasources = async (types: string[]): Promi
export function getPanelDatasourceTypes(scene: DashboardScene): string[] { export function getPanelDatasourceTypes(scene: DashboardScene): string[] {
const types = new Set<string>(); const types = new Set<string>();
const body = scene.state.body; const panels = scene.state.body.getVizPanels();
if (!(body instanceof SceneGridLayout)) {
return [];
}
for (const child of body.state.children) { for (const child of panels) {
if (child instanceof DashboardGridItem) { const ts = panelDatasourceTypes(child);
const ts = panelDatasourceTypes(child); for (const t of ts) {
for (const t of ts) { types.add(t);
types.add(t);
}
}
if (child instanceof SceneGridRow) {
const ts = rowTypes(child);
for (const t of ts) {
types.add(t);
}
} }
} }
return Array.from(types).sort(); return Array.from(types).sort();
} }
function rowTypes(gridRow: SceneGridRow) { function panelDatasourceTypes(vizPanel: VizPanel) {
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;
const types = new Set<string>(); const types = new Set<string>();
if (dataProvider instanceof SceneQueryRunner) {
for (const q of dataProvider.state.queries) {
types.add(q.datasource?.type ?? '');
}
}
if (dataProvider instanceof SceneDataTransformer) { const queryRunner = getQueryRunnerFor(vizPanel);
const panelData = dataProvider.state.$data; if (queryRunner) {
if (panelData instanceof SceneQueryRunner) { for (const q of queryRunner.state.queries) {
for (const q of panelData.state.queries) { types.add(q.datasource?.type ?? '');
types.add(q.datasource?.type ?? '');
}
} }
} }

@ -1,10 +1,8 @@
import { useState, useEffect } from 'react'; 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 { DashboardScene } from '../scene/DashboardScene';
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
import { DashboardRepeatsProcessedEvent } from '../scene/types'; import { DashboardRepeatsProcessedEvent } from '../scene/types';
import { findVizPanelByKey, isPanelClone } from '../utils/utils'; import { findVizPanelByKey, isPanelClone } from '../utils/utils';
@ -63,31 +61,10 @@ function findRepeatClone(dashboard: DashboardScene, panelId: string): Promise<Vi
resolve(panel); resolve(panel);
} else { } else {
// If rows are repeated they could add new panel repeaters that needs to be activated // If rows are repeated they could add new panel repeaters that needs to be activated
activateAllRepeaters(dashboard.state.body); dashboard.state.body.activateRepeaters?.();
} }
}); });
activateAllRepeaters(dashboard.state.body); dashboard.state.body.activateRepeaters?.();
});
}
function activateAllRepeaters(layout: SceneObject) {
layout.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;
}
}
// Activate any panel DashboardGridItem inside the row
activateAllRepeaters(child);
}
}); });
} }

@ -1,7 +1,6 @@
import { TimeRangeUpdatedEvent } from '@grafana/runtime'; import { TimeRangeUpdatedEvent } from '@grafana/runtime';
import { import {
behaviors, behaviors,
SceneGridLayout,
SceneQueryRunner, SceneQueryRunner,
SceneTimeRange, SceneTimeRange,
VizPanel, VizPanel,
@ -14,8 +13,8 @@ import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer';
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
import { DashboardControls } from '../scene/DashboardControls'; import { DashboardControls } from '../scene/DashboardControls';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { NEW_LINK } from '../settings/links/utils'; import { NEW_LINK } from '../settings/links/utils';
import { DashboardModelCompatibilityWrapper } from './DashboardModelCompatibilityWrapper'; import { DashboardModelCompatibilityWrapper } from './DashboardModelCompatibilityWrapper';
@ -97,13 +96,13 @@ describe('DashboardModelCompatibilityWrapper', () => {
}); });
it('Can remove panel', () => { 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)!); 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', () => { it('Checks if annotations are editable', () => {
@ -173,75 +172,59 @@ function setup() {
controls: new DashboardControls({ controls: new DashboardControls({
hideTimeControls: true, hideTimeControls: true,
}), }),
body: new SceneGridLayout({ body: DefaultGridLayoutManager.fromVizPanels([
children: [ new VizPanel({
new DashboardGridItem({ title: 'Panel with a regular data source query',
key: 'griditem-1', key: 'panel-1',
x: 0, pluginId: 'table',
body: new VizPanel({ $data: new SceneQueryRunner({
title: 'Panel with a regular data source query', key: 'data-query-runner',
key: 'panel-1', queries: [{ refId: 'A' }],
pluginId: 'table', datasource: { uid: 'gdev-testdata', type: 'grafana-testdata-datasource' },
$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',
}),
}), }),
}),
new DashboardGridItem({ new VizPanel({
body: new VizPanel({ title: 'Panel with no queries',
title: 'Panel with a shared query', key: 'panel-2',
key: 'panel-3', pluginId: 'table',
pluginId: 'table', }),
$data: new SceneQueryRunner({ new VizPanel({
key: 'data-query-runner', title: 'Panel with a shared query',
queries: [{ refId: 'A', panelId: 1 }], key: 'panel-3',
datasource: { uid: SHARED_DASHBOARD_QUERY, type: 'datasource' }, pluginId: 'table',
}), $data: new SceneQueryRunner({
}), key: 'data-query-runner',
queries: [{ refId: 'A', panelId: 1 }],
datasource: { uid: SHARED_DASHBOARD_QUERY, type: 'datasource' },
}), }),
}),
new DashboardGridItem({ new VizPanel({
body: new VizPanel({ title: 'Panel with a regular data source query and transformations',
title: 'Panel with a regular data source query and transformations', key: 'panel-4',
key: 'panel-4', pluginId: 'table',
pluginId: 'table', $data: new SceneDataTransformer({
$data: new SceneDataTransformer({ $data: new SceneQueryRunner({
$data: new SceneQueryRunner({ key: 'data-query-runner',
key: 'data-query-runner', queries: [{ refId: 'A' }],
queries: [{ refId: 'A' }], datasource: { uid: 'gdev-testdata', type: 'grafana-testdata-datasource' },
datasource: { uid: 'gdev-testdata', type: 'grafana-testdata-datasource' },
}),
transformations: [],
}),
}), }),
transformations: [],
}), }),
new DashboardGridItem({ }),
body: new VizPanel({ new VizPanel({
title: 'Panel with a shared query and transformations', title: 'Panel with a shared query and transformations',
key: 'panel-4', key: 'panel-4',
pluginId: 'table', pluginId: 'table',
$data: new SceneDataTransformer({ $data: new SceneDataTransformer({
$data: new SceneQueryRunner({ $data: new SceneQueryRunner({
key: 'data-query-runner', key: 'data-query-runner',
queries: [{ refId: 'A', panelId: 1 }], queries: [{ refId: 'A', panelId: 1 }],
datasource: { uid: SHARED_DASHBOARD_QUERY, type: 'datasource' }, datasource: { uid: SHARED_DASHBOARD_QUERY, type: 'datasource' },
}),
transformations: [],
}),
}), }),
transformations: [],
}), }),
], }),
}), ]),
}); });
const wrapper = new DashboardModelCompatibilityWrapper(scene); const wrapper = new DashboardModelCompatibilityWrapper(scene);

@ -2,17 +2,8 @@ import { Subscription } from 'rxjs';
import { AnnotationQuery, DashboardCursorSync, dateTimeFormat, DateTimeInput, EventBusSrv } from '@grafana/data'; import { AnnotationQuery, DashboardCursorSync, dateTimeFormat, DateTimeInput, EventBusSrv } from '@grafana/data';
import { TimeRangeUpdatedEvent } from '@grafana/runtime'; import { TimeRangeUpdatedEvent } from '@grafana/runtime';
import { import { behaviors, SceneDataLayerSet, sceneGraph, SceneObject, VizPanel } from '@grafana/scenes';
behaviors,
SceneDataLayerSet,
sceneGraph,
SceneGridLayout,
SceneGridRow,
SceneObject,
VizPanel,
} from '@grafana/scenes';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { dataLayersToAnnotations } from '../serialization/dataLayersToAnnotations'; import { dataLayersToAnnotations } from '../serialization/dataLayersToAnnotations';
@ -175,39 +166,7 @@ export class DashboardModelCompatibilityWrapper {
return; return;
} }
const gridItem = vizPanel.parent; this._scene.removePanel(vizPanel);
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)),
});
}
}
} }
public canEditAnnotations(dashboardUID?: string) { public canEditAnnotations(dashboardUID?: string) {

@ -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 { DashboardCursorSync } from '@grafana/schema';
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
@ -6,10 +6,10 @@ import { DashboardControls } from '../scene/DashboardControls';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; 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'; import { findVizPanelByKey } from './utils';
describe('dashboardSceneGraph', () => { 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', () => { describe('getDataLayers', () => {
let scene: DashboardScene; 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', () => { describe('getCursorSync', () => {
it('should return cursor sync behavior', () => { it('should return cursor sync behavior', () => {
const scene = buildTestScene(); const scene = buildTestScene();
@ -224,7 +59,8 @@ describe('dashboardSceneGraph', () => {
}); });
it('should return undefined if no cursor sync behavior', () => { it('should return undefined if no cursor sync behavior', () => {
const scene = buildTestScene({ $behaviors: [] }); const scene = buildTestScene();
scene.setState({ $behaviors: [] });
const cursorSync = dashboardSceneGraph.getCursorSync(scene); const cursorSync = dashboardSceneGraph.getCursorSync(scene);
expect(cursorSync).toBeUndefined(); expect(cursorSync).toBeUndefined();
@ -259,63 +95,30 @@ function buildTestScene(overrides?: Partial<DashboardSceneState>) {
}), }),
], ],
}), }),
body: new SceneGridLayout({ body: new DefaultGridLayoutManager({
children: [ grid: new SceneGridLayout({
new DashboardGridItem({ children: [
key: 'griditem-1', new DashboardGridItem({
x: 0, key: 'griditem-1',
body: new VizPanel({ x: 0,
title: 'Panel A', body: new VizPanel({
key: 'panel-1', title: 'Panel A',
pluginId: 'table', key: 'panel-1',
$data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), 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',
}),
}), }),
new DashboardGridItem({ }),
body: new VizPanel({ new DashboardGridItem({
title: 'Panel F', body: new VizPanel({
key: 'panel-2-clone-2', title: 'Panel D',
pluginId: 'table', key: 'panel-with-links',
}), pluginId: 'table',
$data: new SceneQueryRunner({ key: 'data-query-runner3', queries: [{ refId: 'A' }] }),
titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })],
}), }),
], }),
}), ],
], }),
}), }),
...overrides, ...overrides,
}); });

@ -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 { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { VizPanelLinks } from '../scene/PanelLinks'; import { VizPanelLinks } from '../scene/PanelLinks';
import { getPanelIdForVizPanel } from './utils';
function getTimePicker(scene: DashboardScene) { function getTimePicker(scene: DashboardScene) {
return scene.state.controls?.state.timePicker; return scene.state.controls?.state.timePicker;
} }
@ -28,29 +25,7 @@ function getPanelLinks(panel: VizPanel) {
} }
function getVizPanels(scene: DashboardScene): VizPanel[] { function getVizPanels(scene: DashboardScene): VizPanel[] {
const panels: VizPanel[] = []; return scene.state.body.getVizPanels();
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;
} }
function getDataLayers(scene: DashboardScene): DashboardDataLayerSet { function getDataLayers(scene: DashboardScene): DashboardDataLayerSet {
@ -74,51 +49,7 @@ export function getCursorSync(scene: DashboardScene) {
} }
export function getNextPanelId(dashboard: DashboardScene): number { export function getNextPanelId(dashboard: DashboardScene): number {
let max = 0; return dashboard.state.body.getNextPanelId();
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;
} }
export const dashboardSceneGraph = { export const dashboardSceneGraph = {

@ -18,6 +18,7 @@ import { DashboardDTO } from 'app/types';
import { DashboardGridItem, RepeatDirection } from '../scene/DashboardGridItem'; import { DashboardGridItem, RepeatDirection } from '../scene/DashboardGridItem';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
export function setupLoadDashboardMock(rsp: DeepPartial<DashboardDTO>, spy?: jest.Mock) { export function setupLoadDashboardMock(rsp: DeepPartial<DashboardDTO>, spy?: jest.Mock) {
const loadDashboardMock = (spy || jest.fn()).mockResolvedValue(rsp); const loadDashboardMock = (spy || jest.fn()).mockResolvedValue(rsp);
@ -184,8 +185,10 @@ export function buildPanelRepeaterScene(options: SceneOptions, source?: VizPanel
$variables: new SceneVariableSet({ $variables: new SceneVariableSet({
variables: [panelRepeatVariable, rowRepeatVariable], variables: [panelRepeatVariable, rowRepeatVariable],
}), }),
body: new SceneGridLayout({ body: new DefaultGridLayoutManager({
children: [row], grid: new SceneGridLayout({
children: [row],
}),
}), }),
}); });

@ -6,7 +6,6 @@ import {
MultiValueVariable, MultiValueVariable,
SceneDataTransformer, SceneDataTransformer,
sceneGraph, sceneGraph,
SceneGridRow,
SceneObject, SceneObject,
SceneQueryRunner, SceneQueryRunner,
VizPanel, VizPanel,
@ -19,7 +18,6 @@ import { DashboardScene } from '../scene/DashboardScene';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { panelMenuBehavior } from '../scene/PanelMenuBehavior'; import { panelMenuBehavior } from '../scene/PanelMenuBehavior';
import { RowActions } from '../scene/row-actions/RowActions';
import { dashboardSceneGraph } from './dashboardSceneGraph'; 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 { export function isLibraryPanel(vizPanel: VizPanel): boolean {
return getLibraryPanelBehavior(vizPanel) !== undefined; return getLibraryPanelBehavior(vizPanel) !== undefined;
} }

Loading…
Cancel
Save