diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index a9e0cfea3be..045ef0799bf 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -15,7 +15,7 @@ import appEvents from 'app/core/app_events'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { VariablesChanged } from 'app/features/variables/types'; -import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; +import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; import { DecoratedRevisionModel } from '../settings/VersionsEditView'; import { historySrv } from '../settings/version-history/HistorySrv'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; @@ -26,6 +26,7 @@ import { DashboardScene, DashboardSceneState } from './DashboardScene'; jest.mock('../settings/version-history/HistorySrv'); jest.mock('../serialization/transformSaveModelToScene'); +jest.mock('../serialization/transformSceneToSaveModel'); jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getDataSourceSrv: () => { @@ -133,7 +134,7 @@ describe('DashboardScene', () => { it('Should add a new panel to the dashboard', () => { const vizPanel = new VizPanel({ title: 'Panel Title', - key: 'panel-4', + key: 'panel-5', pluginId: 'timeseries', $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), }); @@ -143,9 +144,9 @@ describe('DashboardScene', () => { const body = scene.state.body as SceneGridLayout; const gridItem = body.state.children[0] as SceneGridItem; - expect(scene.state.isDirty).toBe(true); expect(body.state.children.length).toBe(5); - expect(gridItem.state.body!.state.key).toBe('panel-4'); + 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', () => { @@ -154,9 +155,114 @@ describe('DashboardScene', () => { const body = scene.state.body as SceneGridLayout; const gridItem = body.state.children[0] as SceneGridItem; - expect(scene.state.isDirty).toBe(true); expect(body.state.children.length).toBe(5); - expect(gridItem.state.body!.state.key).toBe('panel-4'); + expect(gridItem.state.body!.state.key).toBe('panel-5'); + }); + + 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(body.state.children.length).toBe(3); + expect(gridRow.state.key).toBe('panel-5'); + expect(gridRow.state.children[0].state.key).toBe('griditem-1'); + expect(gridRow.state.children[1].state.key).toBe('griditem-2'); + }); + + it('Should create a row and add all panels in the dashboard under it', () => { + const scene = buildTestScene({ + body: new SceneGridLayout({ + children: [ + new SceneGridItem({ + 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 SceneGridItem({ + 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(4); + 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 copy a panel', () => { + const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as SceneGridItem).state.body; + scene.copyPanel(vizPanel as VizPanel); + + expect(scene.state.hasCopiedPanel).toBe(true); + }); + + it('Should paste a panel', () => { + scene.setState({ hasCopiedPanel: true }); + jest.spyOn(JSON, 'parse').mockReturnThis(); + jest.mocked(buildGridItemForPanel).mockReturnValue( + new SceneGridItem({ + key: 'griditem-9', + body: new VizPanel({ + title: 'Panel A', + key: 'panel-9', + pluginId: 'table', + }), + }) + ); + + scene.pastePanel(); + + const body = scene.state.body as SceneGridLayout; + const gridItem = body.state.children[0] as SceneGridItem; + + expect(body.state.children.length).toBe(5); + expect(gridItem.state.body!.state.key).toBe('panel-5'); + expect(gridItem.state.y).toBe(0); + expect(scene.state.hasCopiedPanel).toBe(false); }); }); }); @@ -275,6 +381,7 @@ function buildTestScene(overrides?: Partial) { }), }), new SceneGridItem({ + key: 'griditem-2', body: new VizPanel({ title: 'Panel B', key: 'panel-2', @@ -282,12 +389,12 @@ function buildTestScene(overrides?: Partial) { }), }), new SceneGridRow({ - key: 'gridrow-1', + key: 'panel-3', children: [ new SceneGridItem({ body: new VizPanel({ title: 'Panel C', - key: 'panel-3', + key: 'panel-4', pluginId: 'table', }), }), diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 9bd46d81d62..5bfbe60a68c 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -11,6 +11,7 @@ import { sceneGraph, SceneGridItem, SceneGridLayout, + SceneGridRow, SceneObject, SceneObjectBase, SceneObjectState, @@ -28,7 +29,7 @@ import { LS_PANEL_COPY_KEY } from 'app/core/constants'; import { getNavModel } from 'app/core/selectors/navModel'; import store from 'app/core/store'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; -import { DashboardModel } from 'app/features/dashboard/state'; +import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { VariablesChanged } from 'app/features/variables/types'; import { DashboardDTO, DashboardMeta, SaveDashboardResponseDTO } from 'app/types'; @@ -37,12 +38,13 @@ import { ShowConfirmModalEvent } from 'app/types/events'; import { PanelEditor } from '../panel-edit/PanelEditor'; import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer'; import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer'; -import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; +import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; import { gridItemToPanel } from '../serialization/transformSceneToSaveModel'; import { DecoratedRevisionModel } from '../settings/VersionsEditView'; import { DashboardEditView } from '../settings/utils'; import { historySrv } from '../settings/version-history'; import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper'; +import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { djb2Hash } from '../utils/djb2Hash'; import { getDashboardUrl } from '../utils/urlBuilders'; import { @@ -50,8 +52,10 @@ import { NEW_PANEL_WIDTH, forceRenderChildren, getClosestVizPanel, + getDefaultRow, getDefaultVizPanel, getPanelIdForVizPanel, + getVizPanelKeyForPanelId, isPanelClone, } from '../utils/utils'; @@ -102,6 +106,8 @@ export interface DashboardSceneState extends SceneObjectState { editPanel?: PanelEditor; /** Scene object that handles the current drawer or modal */ overlay?: SceneObject; + /** True when a user copies a panel in the dashboard */ + hasCopiedPanel?: boolean; isEmpty?: boolean; } @@ -142,6 +148,7 @@ export class DashboardScene extends SceneObjectBase { editable: true, body: state.body ?? new SceneFlexLayout({ children: [] }), links: state.links ?? [], + hasCopiedPanel: store.exists(LS_PANEL_COPY_KEY), ...state, }); @@ -423,6 +430,31 @@ export class DashboardScene extends SceneObjectBase { return this._initialState; } + public addRow(row: SceneGridRow) { + if (!(this.state.body instanceof SceneGridLayout)) { + throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); + } + + const sceneGridLayout = this.state.body; + + // find all panels until the first row and put them into the newly created row. If there are no other rows, + // add all panels to the row. If there are no panels just create an empty row + const indexTillNextRow = sceneGridLayout.state.children.findIndex((child) => child instanceof SceneGridRow); + const rowChildren = sceneGridLayout.state.children + .splice(0, indexTillNextRow === -1 ? sceneGridLayout.state.children.length : indexTillNextRow) + .map((child) => child.clone()); + + if (rowChildren) { + row.setState({ + children: rowChildren, + }); + } + + sceneGridLayout.setState({ + children: [row, ...sceneGridLayout.state.children], + }); + } + public addPanel(vizPanel: VizPanel): void { if (!(this.state.body instanceof SceneGridLayout)) { throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); @@ -430,19 +462,14 @@ export class DashboardScene extends SceneObjectBase { const sceneGridLayout = this.state.body; - // move all gridItems below the new one - for (const child of sceneGridLayout.state.children) { - child.setState({ - y: NEW_PANEL_HEIGHT + (child.state.y ?? 0), - }); - } - + const panelId = getPanelIdForVizPanel(vizPanel); const newGridItem = new SceneGridItem({ height: NEW_PANEL_HEIGHT, width: NEW_PANEL_WIDTH, x: 0, y: 0, body: vizPanel, + key: `grid-item-${panelId}`, }); sceneGridLayout.setState({ @@ -457,7 +484,7 @@ export class DashboardScene extends SceneObjectBase { const gridItem = vizPanel.parent; - if (!(gridItem instanceof SceneGridItem || PanelRepeaterGridItem)) { + if (!(gridItem instanceof SceneGridItem || gridItem instanceof PanelRepeaterGridItem)) { console.error('Trying to duplicate a panel in a layout that is not SceneGridItem or PanelRepeaterGridItem'); return; } @@ -506,7 +533,52 @@ export class DashboardScene extends SceneObjectBase { const jsonData = gridItemToPanel(gridItem); store.set(LS_PANEL_COPY_KEY, JSON.stringify(jsonData)); - appEvents.emit(AppEvents.alertSuccess, ['Panel copied. Click **Add panel** icon to paste.']); + appEvents.emit(AppEvents.alertSuccess, ['Panel copied. Use **Paste panel** toolbar action to paste.']); + this.setState({ hasCopiedPanel: true }); + } + + public pastePanel() { + if (!(this.state.body instanceof SceneGridLayout)) { + throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); + } + + const jsonData = store.get(LS_PANEL_COPY_KEY); + const jsonObj = JSON.parse(jsonData); + const panelModel = new PanelModel(jsonObj); + + const gridItem = buildGridItemForPanel(panelModel); + const sceneGridLayout = this.state.body; + + if (!(gridItem instanceof SceneGridItem) && !(gridItem instanceof PanelRepeaterGridItem)) { + throw new Error('Cannot paste invalid grid item'); + } + + const panelId = dashboardSceneGraph.getNextPanelId(this); + + if (gridItem instanceof SceneGridItem && gridItem.state.body) { + gridItem.state.body.setState({ + key: getVizPanelKeyForPanelId(panelId), + }); + } else if (gridItem instanceof PanelRepeaterGridItem) { + gridItem.state.source.setState({ + key: getVizPanelKeyForPanelId(panelId), + }); + } + + gridItem.setState({ + height: NEW_PANEL_HEIGHT, + width: NEW_PANEL_WIDTH, + x: 0, + y: 0, + key: `grid-item-${panelId}`, + }); + + sceneGridLayout.setState({ + children: [gridItem, ...sceneGridLayout.state.children], + }); + + this.setState({ hasCopiedPanel: false }); + store.delete(LS_PANEL_COPY_KEY); } public showModal(modal: SceneObject) { @@ -540,6 +612,14 @@ export class DashboardScene extends SceneObjectBase { locationService.partial({ editview: 'settings' }); }; + public onCreateNewRow() { + const row = getDefaultRow(this); + + this.addRow(row); + + return getPanelIdForVizPanel(row); + } + public onCreateNewPanel(): number { const vizPanel = getDefaultVizPanel(this); diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx index 6d95fcccca3..0c192c1df9a 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx @@ -16,6 +16,8 @@ describe('NavToolbarActions', () => { expect(screen.queryByText('Save dashboard')).not.toBeInTheDocument(); expect(screen.queryByLabelText('Add visualization')).not.toBeInTheDocument(); + expect(screen.queryByLabelText('Add row')).not.toBeInTheDocument(); + expect(screen.queryByLabelText('Paste panel')).not.toBeInTheDocument(); expect(await screen.findByText('Edit')).toBeInTheDocument(); expect(await screen.findByText('Share')).toBeInTheDocument(); }); @@ -28,6 +30,8 @@ describe('NavToolbarActions', () => { expect(await screen.findByText('Save dashboard')).toBeInTheDocument(); expect(await screen.findByText('Exit edit')).toBeInTheDocument(); expect(await screen.findByLabelText('Add visualization')).toBeInTheDocument(); + expect(await screen.findByLabelText('Add row')).toBeInTheDocument(); + expect(await screen.findByLabelText('Paste panel')).toBeInTheDocument(); expect(screen.queryByText('Edit')).not.toBeInTheDocument(); expect(screen.queryByText('Share')).not.toBeInTheDocument(); }); diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 02164f9f1f7..76215619b74 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -37,12 +37,22 @@ NavToolbarActions.displayName = 'NavToolbarActions'; * This part is split into a separate component to help test this */ export function ToolbarActions({ dashboard }: Props) { - const { isEditing, viewPanelScene, isDirty, uid, meta, editview, editPanel } = dashboard.useState(); + const { + isEditing, + viewPanelScene, + isDirty, + uid, + meta, + editview, + editPanel, + hasCopiedPanel: copiedPanel, + } = dashboard.useState(); const canSaveAs = contextSrv.hasEditPermissionInFolders; const toolbarActions: ToolbarAction[] = []; const buttonWithExtraMargin = useStyles2(getStyles); const isEditingPanel = Boolean(editPanel); const isViewingPanel = Boolean(viewPanelScene); + const hasCopiedPanel = Boolean(copiedPanel); toolbarActions.push({ group: 'icon-actions', @@ -61,6 +71,39 @@ export function ToolbarActions({ dashboard }: Props) { ), }); + toolbarActions.push({ + group: 'icon-actions', + condition: isEditing && !editview && !isViewingPanel && !isEditingPanel, + render: () => ( + { + dashboard.onCreateNewRow(); + DashboardInteractions.toolbarAddButtonClicked({ item: 'add_row' }); + }} + /> + ), + }); + + toolbarActions.push({ + group: 'icon-actions', + condition: isEditing && !editview && !isViewingPanel && !isEditingPanel, + render: () => ( + { + dashboard.pastePanel(); + DashboardInteractions.toolbarAddButtonClicked({ item: 'paste_panel' }); + }} + /> + ), + }); + toolbarActions.push({ group: 'icon-actions', condition: uid && !editview && Boolean(meta.canStar) && !isEditingPanel && !isEditing, diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts index 5e106d15d90..a799efe2c38 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts @@ -12,9 +12,10 @@ import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardControls } from '../scene/DashboardControls'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; -import { dashboardSceneGraph } from './dashboardSceneGraph'; +import { dashboardSceneGraph, getNextPanelId } from './dashboardSceneGraph'; import { findVizPanelByKey } from './utils'; describe('dashboardSceneGraph', () => { @@ -84,6 +85,120 @@ describe('dashboardSceneGraph', () => { expect(() => dashboardSceneGraph.getDataLayers(scene)).toThrow('SceneDataLayers not found'); }); }); + + describe('getNextPanelId', () => { + it('should get next panel id in a simple 3 panel layout', () => { + const scene = buildTestScene({ + body: new SceneGridLayout({ + children: [ + new SceneGridItem({ + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + }), + }), + new SceneGridItem({ + body: new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', + }), + }), + new SceneGridItem({ + body: new VizPanel({ + title: 'Panel C', + key: 'panel-3', + pluginId: 'table', + }), + }), + ], + }), + }); + + const id = getNextPanelId(scene); + + expect(id).toBe(4); + }); + + it('should take library panels into account', () => { + const scene = buildTestScene({ + body: new SceneGridLayout({ + children: [ + new SceneGridItem({ + key: 'griditem-1', + x: 0, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + }), + }), + new SceneGridItem({ + body: new LibraryVizPanel({ + uid: 'uid', + name: 'LibPanel', + title: 'Library Panel', + panelKey: 'panel-2', + }), + }), + new SceneGridItem({ + body: new VizPanel({ + title: 'Panel C', + key: 'panel-2-clone-1', + pluginId: 'table', + }), + }), + new SceneGridRow({ + key: 'key', + title: 'row', + children: [ + new SceneGridItem({ + body: new VizPanel({ + title: 'Panel E', + key: 'panel-2-clone-2', + pluginId: 'table', + }), + }), + new SceneGridItem({ + body: new LibraryVizPanel({ + uid: 'uid', + name: 'LibPanel', + title: 'Library Panel', + panelKey: 'panel-3', + }), + }), + ], + }), + ], + }), + }); + + const id = getNextPanelId(scene); + + expect(id).toBe(4); + }); + + 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'); + }); + }); }); function buildTestScene(overrides?: Partial) { diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts index 3047692da5b..898885a7304 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts @@ -1,8 +1,11 @@ -import { VizPanel, SceneGridItem, SceneGridRow, SceneDataLayers, sceneGraph } from '@grafana/scenes'; +import { VizPanel, SceneGridItem, SceneGridRow, SceneDataLayers, sceneGraph, SceneGridLayout } from '@grafana/scenes'; import { DashboardScene } from '../scene/DashboardScene'; +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { VizPanelLinks } from '../scene/PanelLinks'; +import { getPanelIdForLibraryVizPanel, getPanelIdForVizPanel } from './utils'; + function getTimePicker(scene: DashboardScene) { return scene.state.controls?.state.timePicker; } @@ -55,10 +58,65 @@ function getDataLayers(scene: DashboardScene): SceneDataLayers { return data; } +export function getNextPanelId(dashboard: DashboardScene): number { + let max = 0; + const body = dashboard.state.body; + + if (!(body instanceof SceneGridLayout)) { + throw new Error('Dashboard body is not a SceneGridLayout'); + } + + for (const child of body.state.children) { + if (child instanceof SceneGridItem) { + const vizPanel = child.state.body; + + if (vizPanel) { + const panelId = + vizPanel instanceof LibraryVizPanel + ? getPanelIdForLibraryVizPanel(vizPanel) + : 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 SceneGridItem) { + const vizPanel = rowChild.state.body; + + if (vizPanel) { + const panelId = + vizPanel instanceof LibraryVizPanel + ? getPanelIdForLibraryVizPanel(vizPanel) + : getPanelIdForVizPanel(vizPanel); + + if (panelId > max) { + max = panelId; + } + } + } + } + } + } + + return max + 1; +} + export const dashboardSceneGraph = { getTimePicker, getRefreshPicker, getPanelLinks, getVizPanels, getDataLayers, + getNextPanelId, }; diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 4ddf171e05c..287668e1262 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -4,8 +4,6 @@ import { MultiValueVariable, SceneDataTransformer, sceneGraph, - SceneGridItem, - SceneGridLayout, SceneGridRow, SceneObject, SceneQueryRunner, @@ -19,6 +17,8 @@ import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelMenuBehavior } from '../scene/PanelMenuBehavior'; +import { dashboardSceneGraph } from './dashboardSceneGraph'; + export const NEW_PANEL_HEIGHT = 8; export const NEW_PANEL_WIDTH = 12; @@ -30,8 +30,12 @@ export function getPanelIdForVizPanel(panel: SceneObject): number { return parseInt(panel.state.key!.replace('panel-', ''), 10); } +export function getPanelIdForLibraryVizPanel(panel: LibraryVizPanel): number { + return parseInt(panel.state.panelKey!.replace('panel-', ''), 10); +} + /** - * This will also try lookup based on panelId + * This will also try lookup based on panelId */ export function findVizPanelByKey(scene: SceneObject, key: string | undefined): VizPanel | null { if (!key) { @@ -201,47 +205,8 @@ export function isPanelClone(key: string) { return key.includes('clone'); } -export function getNextPanelId(dashboard: DashboardScene) { - let max = 0; - const body = dashboard.state.body; - - if (body instanceof SceneGridLayout) { - for (const child of body.state.children) { - if (child instanceof SceneGridItem) { - const vizPanel = child.state.body; - - if (vizPanel instanceof VizPanel) { - const panelId = getPanelIdForVizPanel(vizPanel); - - if (panelId > max) { - max = panelId; - } - } - } - - if (child instanceof SceneGridRow) { - for (const rowChild of child.state.children) { - if (rowChild instanceof SceneGridItem) { - const vizPanel = rowChild.state.body; - - if (vizPanel instanceof VizPanel) { - const panelId = getPanelIdForVizPanel(vizPanel); - - if (panelId > max) { - max = panelId; - } - } - } - } - } - } - } - - return max + 1; -} - export function getDefaultVizPanel(dashboard: DashboardScene): VizPanel { - const panelId = getNextPanelId(dashboard); + const panelId = dashboardSceneGraph.getNextPanelId(dashboard); return new VizPanel({ title: 'Panel Title', @@ -261,6 +226,16 @@ 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', + y: 0, + }); +} + export function isLibraryPanelChild(vizPanel: VizPanel) { return vizPanel.parent instanceof LibraryVizPanel; }