From 0916994d0af3107853393557ab5153414a7f4fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 6 Feb 2025 10:57:08 +0100 Subject: [PATCH] Dashboard: Various fixes to new layouts (#100107) * Dashboard: Various fixes to new layouts * review fixes * Fix * Update * Fix test --- .../dashboard-scene/scene/DashboardScene.tsx | 16 ++--- .../DefaultGridLayoutManager.test.tsx | 16 ----- .../DefaultGridLayoutManager.tsx | 56 ++--------------- .../ResponsiveGridLayoutManager.tsx | 63 +++++++++++-------- .../scene/layout-rows/RowsLayoutManager.tsx | 8 --- .../scene/types/DashboardLayoutManager.ts | 5 -- .../utils/dashboardSceneGraph.test.ts | 24 ++++++- .../utils/dashboardSceneGraph.ts | 29 ++++++++- 8 files changed, 99 insertions(+), 118 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index e015172d4c6..29c9380039c 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -58,7 +58,13 @@ import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { djb2Hash } from '../utils/djb2Hash'; import { getDashboardUrl } from '../utils/getDashboardUrl'; import { getViewPanelUrl } from '../utils/urlBuilders'; -import { getClosestVizPanel, getDashboardSceneFor, getDefaultVizPanel, getPanelIdForVizPanel } from '../utils/utils'; +import { + getClosestVizPanel, + getDashboardSceneFor, + getDefaultVizPanel, + getLayoutManagerFor, + getPanelIdForVizPanel, +} from '../utils/utils'; import { SchemaV2EditorDrawer } from '../v2schema/SchemaV2EditorDrawer'; import { AddLibraryPanelDrawer } from './AddLibraryPanelDrawer'; @@ -475,10 +481,6 @@ export class DashboardScene extends SceneObjectBase { return this._initialState; } - public getNextPanelId(): number { - return this.state.body.getMaxPanelId() + 1; - } - public addPanel(vizPanel: VizPanel): void { if (!this.state.isEditing) { this.onEnterEditMode(); @@ -502,7 +504,7 @@ export class DashboardScene extends SceneObjectBase { } public duplicatePanel(vizPanel: VizPanel) { - this.state.body.duplicatePanel(vizPanel); + getLayoutManagerFor(vizPanel).duplicatePanel(vizPanel); } public copyPanel(vizPanel: VizPanel) { @@ -536,7 +538,7 @@ export class DashboardScene extends SceneObjectBase { } public removePanel(panel: VizPanel) { - this.state.body.removePanel(panel); + getLayoutManagerFor(panel).removePanel(panel); } public unlinkLibraryPanel(panel: VizPanel) { diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx index 8a06e7c9031..fdb9dd9fc38 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx @@ -26,22 +26,6 @@ describe('DefaultGridLayoutManager', () => { }); }); - describe('getMaxPanelId', () => { - it('should get max panel id in a simple 3 panel layout', () => { - const { manager } = setup(); - const id = manager.getMaxPanelId(); - - expect(id).toBe(3); - }); - - it('should return 0 if no panels are found', () => { - const { manager } = setup({ gridItems: [] }); - const id = manager.getMaxPanelId(); - - expect(id).toBe(0); - }); - }); - describe('addPanel', () => { it('Should add a new panel', () => { const { manager } = setup(); diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx index d714bc87d4d..c7389618108 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -14,6 +14,7 @@ import { GRID_COLUMN_COUNT } from 'app/core/constants'; import { t } from 'app/core/internationalization'; import { isClonedKey, joinCloneKeys } from '../../utils/clone'; +import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph'; import { forceRenderChildren, getPanelIdForVizPanel, @@ -21,7 +22,6 @@ import { NEW_PANEL_WIDTH, getVizPanelKeyForPanelId, getGridItemKeyForPanelId, - getDashboardSceneFor, } from '../../utils/utils'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; @@ -72,7 +72,7 @@ export class DefaultGridLayoutManager } public addPanel(vizPanel: VizPanel): void { - const panelId = this.getNextPanelId(); + const panelId = dashboardSceneGraph.getNextPanelId(this); vizPanel.setState({ key: getVizPanelKeyForPanelId(panelId) }); vizPanel.clearParent(); @@ -95,7 +95,8 @@ export class DefaultGridLayoutManager * Adds a new empty row */ public addNewRow(): SceneGridRow { - const id = this.getNextPanelId(); + const id = dashboardSceneGraph.getNextPanelId(this); + const row = new SceneGridRow({ key: getVizPanelKeyForPanelId(id), title: 'Row title', @@ -183,7 +184,7 @@ export class DefaultGridLayoutManager let panelData; let newGridItem; - const newPanelId = this.getNextPanelId(); + const newPanelId = dashboardSceneGraph.getNextPanelId(this); const grid = this.state.grid; if (gridItem instanceof DashboardGridItem) { @@ -248,53 +249,6 @@ export class DefaultGridLayoutManager return panels; } - public getMaxPanelId(): 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; - } - - public getNextPanelId(): number { - return getDashboardSceneFor(this).getNextPanelId(); - } - public collapseAllRows(): void { this.state.grid.state.children.forEach((child) => { if (!(child instanceof SceneGridRow)) { diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx index a1e4798b6c2..91fcbec0a48 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx @@ -4,7 +4,8 @@ import { Select } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; -import { getDashboardSceneFor, getPanelIdForVizPanel, getVizPanelKeyForPanelId } from '../../utils/utils'; +import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph'; +import { getDashboardSceneFor, getGridItemKeyForPanelId, getVizPanelKeyForPanelId } from '../../utils/utils'; import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; @@ -33,10 +34,17 @@ export class ResponsiveGridLayoutManager public readonly descriptor = ResponsiveGridLayoutManager.descriptor; + public constructor(state: ResponsiveGridLayoutManagerState) { + super(state); + + //@ts-ignore + this.state.layout.getDragClassCancel = () => 'drag-cancel'; + } + public editModeChanged(isEditing: boolean): void {} public addPanel(vizPanel: VizPanel): void { - const panelId = this.getNextPanelId(); + const panelId = dashboardSceneGraph.getNextPanelId(this); vizPanel.setState({ key: getVizPanelKeyForPanelId(panelId) }); vizPanel.clearParent(); @@ -52,33 +60,35 @@ export class ResponsiveGridLayoutManager getDashboardSceneFor(this).switchLayout(rowsLayout); } - public getMaxPanelId(): number { - let max = 0; - - for (const child of this.state.layout.state.children) { - if (child instanceof VizPanel) { - let panelId = getPanelIdForVizPanel(child); - - if (panelId > max) { - max = panelId; - } - } - } - - return max; - } - - public getNextPanelId(): number { - return getDashboardSceneFor(this).getNextPanelId(); - } - public removePanel(panel: VizPanel) { const element = panel.parent; this.state.layout.setState({ children: this.state.layout.state.children.filter((child) => child !== element) }); } public duplicatePanel(panel: VizPanel): void { - throw new Error('Method not implemented.'); + const gridItem = panel.parent; + if (!(gridItem instanceof ResponsiveGridItem)) { + console.error('Trying to duplicate a panel that is not inside a DashboardGridItem'); + return; + } + + const newPanelId = dashboardSceneGraph.getNextPanelId(this); + const grid = this.state.layout; + + const newGridItem = gridItem.clone({ + key: getGridItemKeyForPanelId(newPanelId), + body: panel.clone({ + key: getVizPanelKeyForPanelId(newPanelId), + }), + }); + + const sourceIndex = grid.state.children.indexOf(gridItem); + const newChildren = [...grid.state.children]; + + // insert after + newChildren.splice(sourceIndex + 1, 0, newGridItem); + + grid.setState({ children: newChildren }); } public getVizPanels(): VizPanel[] { @@ -124,9 +134,10 @@ export class ResponsiveGridLayoutManager }); } - activateRepeaters?(): void { - throw new Error('Method not implemented.'); - } + /** + * Might as well implement this as a no-top function as vs-code very eagerly adds optional functions that throw Method not implemented + */ + public activateRepeaters(): void {} public static Component = ({ model }: SceneComponentProps) => { return ; diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx index 10ff3f66550..dc9154d3b2b 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -75,14 +75,6 @@ export class RowsLayoutManager extends SceneObjectBase i }); } - public getMaxPanelId(): number { - return Math.max(...this.state.rows.map((row) => row.getLayout().getMaxPanelId())); - } - - public getNextPanelId(): number { - return 0; - } - public removePanel(panel: VizPanel) {} public removeRow(row: RowItem) { diff --git a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts index 572d7da9549..fbcd4200d36 100644 --- a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts +++ b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts @@ -38,11 +38,6 @@ export interface DashboardLayoutManager extends SceneObject { */ getVizPanels(): VizPanel[]; - /** - * Returns the highest panel id in the layout - */ - getMaxPanelId(): number; - /** * Add row */ diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts index 6e0b078ea22..2aa39746339 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts @@ -9,7 +9,7 @@ import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; -import { dashboardSceneGraph } from './dashboardSceneGraph'; +import { dashboardSceneGraph, getNextPanelId } from './dashboardSceneGraph'; import { findVizPanelByKey } from './utils'; describe('dashboardSceneGraph', () => { @@ -22,7 +22,7 @@ describe('dashboardSceneGraph', () => { it('should resolve VizPanelLinks object', () => { const scene = buildTestScene(); - const panelWithNoLinks = findVizPanelByKey(scene, 'panel-with-links')!; + const panelWithNoLinks = findVizPanelByKey(scene, 'panel-2')!; expect(dashboardSceneGraph.getPanelLinks(panelWithNoLinks)).toBeInstanceOf(VizPanelLinks); }); }); @@ -66,6 +66,24 @@ describe('dashboardSceneGraph', () => { expect(cursorSync).toBeUndefined(); }); }); + + describe('getNextPanelId', () => { + it('should get next panel id in a simple 3 panel layout', () => { + const scene = buildTestScene(); + const id = getNextPanelId(scene); + + expect(id).toBe(3); + }); + + it('should return 1 if no panels are found', () => { + const scene = buildTestScene(); + + const grid = scene.state.body as DefaultGridLayoutManager; + grid.state.grid.setState({ children: [] }); + const id = getNextPanelId(scene); + expect(id).toBe(1); + }); + }); }); function buildTestScene(overrides?: Partial) { @@ -111,7 +129,7 @@ function buildTestScene(overrides?: Partial) { new DashboardGridItem({ body: new VizPanel({ title: 'Panel D', - key: 'panel-with-links', + key: 'panel-2', pluginId: 'table', $data: new SceneQueryRunner({ key: 'data-query-runner3', queries: [{ refId: 'A' }] }), titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })], diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts index 10edc9904d7..8d1a8176754 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts @@ -1,10 +1,11 @@ -import { VizPanel, sceneGraph, behaviors } from '@grafana/scenes'; +import { VizPanel, sceneGraph, behaviors, SceneObject, SceneGridRow } from '@grafana/scenes'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardScene } from '../scene/DashboardScene'; import { VizPanelLinks } from '../scene/PanelLinks'; -import { getLayoutManagerFor } from './utils'; +import { isClonedKey } from './clone'; +import { getLayoutManagerFor, getPanelIdForVizPanel } from './utils'; function getTimePicker(scene: DashboardScene) { return scene.state.controls?.state.timePicker; @@ -28,6 +29,29 @@ function getVizPanels(scene: DashboardScene): VizPanel[] { return scene.state.body.getVizPanels(); } +/** + * Will look for all panels in the entire scene starting from root + * and find the next free panel id + */ +export function getNextPanelId(scene: SceneObject): number { + let max = 0; + + sceneGraph + .findAllObjects(scene.getRoot(), (obj) => obj instanceof VizPanel || obj instanceof SceneGridRow) + .forEach((panel) => { + if (isClonedKey(panel.state.key!)) { + return; + } + + const panelId = getPanelIdForVizPanel(panel); + if (panelId > max) { + max = panelId; + } + }); + + return max + 1; +} + function getDataLayers(scene: DashboardScene): DashboardDataLayerSet { const data = sceneGraph.getData(scene); @@ -56,4 +80,5 @@ export const dashboardSceneGraph = { getDataLayers, getCursorSync, getLayoutManagerFor, + getNextPanelId, };