Dashboard: Various fixes to new layouts (#100107)

* Dashboard: Various fixes to new layouts

* review fixes

* Fix

* Update

* Fix test
This commit is contained in:
Torkel Ödegaard 2025-02-06 10:57:08 +01:00 committed by GitHub
parent d5f1f4eb5c
commit 0916994d0a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 99 additions and 118 deletions

View File

@ -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<DashboardSceneState> {
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<DashboardSceneState> {
}
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<DashboardSceneState> {
}
public removePanel(panel: VizPanel) {
this.state.body.removePanel(panel);
getLayoutManagerFor(panel).removePanel(panel);
}
public unlinkLibraryPanel(panel: VizPanel) {

View File

@ -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();

View File

@ -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)) {

View File

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

View File

@ -75,14 +75,6 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> 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) {

View File

@ -38,11 +38,6 @@ export interface DashboardLayoutManager<S = {}> extends SceneObject {
*/
getVizPanels(): VizPanel[];
/**
* Returns the highest panel id in the layout
*/
getMaxPanelId(): number;
/**
* Add row
*/

View File

@ -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<DashboardSceneState>) {
@ -111,7 +129,7 @@ function buildTestScene(overrides?: Partial<DashboardSceneState>) {
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({}) })],

View File

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