Scenes: Duplicate library panels (#84159)

duplicate library panels
This commit is contained in:
Victor Marin 2024-03-12 10:11:15 +02:00 committed by GitHub
parent 298384cea9
commit f50624d257
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 209 additions and 22 deletions

View File

@ -30,6 +30,7 @@ import { djb2Hash } from '../utils/djb2Hash';
import { DashboardControls } from './DashboardControls';
import { DashboardScene, DashboardSceneState } from './DashboardScene';
import { LibraryVizPanel } from './LibraryVizPanel';
import { PanelRepeaterGridItem } from './PanelRepeaterGridItem';
jest.mock('../settings/version-history/HistorySrv');
jest.mock('../serialization/transformSaveModelToScene');
@ -541,7 +542,119 @@ describe('DashboardScene', () => {
expect(gridRow.state.children.length).toBe(1);
});
it('Should unlink a library panel', () => {
it('Should duplicate a panel', () => {
const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as SceneGridItem).state.body;
scene.duplicatePanel(vizPanel as VizPanel);
const body = scene.state.body as SceneGridLayout;
const gridItem = body.state.children[5] as SceneGridItem;
expect(body.state.children.length).toBe(6);
expect(gridItem.state.body!.state.key).toBe('panel-7');
});
it('Should duplicate a library panel', () => {
const libraryPanel = ((scene.state.body as SceneGridLayout).state.children[4] as SceneGridItem).state.body;
const vizPanel = (libraryPanel as LibraryVizPanel).state.panel;
scene.duplicatePanel(vizPanel as VizPanel);
const body = scene.state.body as SceneGridLayout;
const gridItem = body.state.children[5] as SceneGridItem;
const libVizPanel = gridItem.state.body as LibraryVizPanel;
expect(body.state.children.length).toBe(6);
expect(libVizPanel.state.panelKey).toBe('panel-7');
expect(libVizPanel.state.panel?.state.key).toBe('panel-7');
});
it('Should duplicate a repeated panel', () => {
const scene = buildTestScene({
body: new SceneGridLayout({
children: [
new PanelRepeaterGridItem({
key: `grid-item-1`,
width: 24,
height: 8,
repeatedPanels: [
new VizPanel({
title: 'Library Panel',
key: 'panel-1',
pluginId: 'table',
}),
],
source: new VizPanel({
title: 'Library Panel',
key: 'panel-1',
pluginId: 'table',
}),
variableName: 'custom',
}),
],
}),
});
const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as PanelRepeaterGridItem).state
.repeatedPanels![0];
scene.duplicatePanel(vizPanel as VizPanel);
const body = scene.state.body as SceneGridLayout;
const gridItem = body.state.children[1] as SceneGridItem;
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 SceneGridItem
).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 SceneGridItem;
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 SceneGridItem
).state.body;
const vizPanel = (libraryPanel as LibraryVizPanel).state.panel;
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 SceneGridItem;
const libVizPanel = gridItem.state.body as LibraryVizPanel;
expect(gridRow.state.children.length).toBe(3);
expect(libVizPanel.state.panelKey).toBe('panel-7');
expect(libVizPanel.state.panel?.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', () => {
const libPanel = new LibraryVizPanel({
title: 'title',
uid: 'abc',

View File

@ -43,7 +43,7 @@ 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 { dashboardSceneGraph, getLibraryVizPanelFromVizPanel } from '../utils/dashboardSceneGraph';
import { djb2Hash } from '../utils/djb2Hash';
import { getDashboardUrl } from '../utils/urlBuilders';
import {
@ -459,7 +459,9 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
return;
}
const gridItem = vizPanel.parent;
const libraryPanel = getLibraryVizPanelFromVizPanel(vizPanel);
const gridItem = libraryPanel ? libraryPanel.parent : vizPanel.parent;
if (!(gridItem instanceof SceneGridItem || gridItem instanceof PanelRepeaterGridItem)) {
console.error('Trying to duplicate a panel in a layout that is not SceneGridItem or PanelRepeaterGridItem');
@ -468,26 +470,45 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
let panelState;
let panelData;
if (gridItem instanceof PanelRepeaterGridItem) {
const { key, ...gridRepeaterSourceState } = sceneUtils.cloneSceneObjectState(gridItem.state.source.state);
panelState = { ...gridRepeaterSourceState };
panelData = sceneGraph.getData(gridItem.state.source).clone();
let newGridItem;
const newPanelId = dashboardSceneGraph.getNextPanelId(this);
if (libraryPanel) {
const gridItemToDuplicateState = sceneUtils.cloneSceneObjectState(gridItem.state);
newGridItem = new SceneGridItem({
x: gridItemToDuplicateState.x,
y: gridItemToDuplicateState.y,
width: gridItemToDuplicateState.width,
height: gridItemToDuplicateState.height,
body: new LibraryVizPanel({
title: libraryPanel.state.title,
uid: libraryPanel.state.uid,
name: libraryPanel.state.name,
panelKey: getVizPanelKeyForPanelId(newPanelId),
}),
});
} else {
const { key, ...gridItemPanelState } = sceneUtils.cloneSceneObjectState(vizPanel.state);
panelState = { ...gridItemPanelState };
panelData = sceneGraph.getData(vizPanel).clone();
if (gridItem instanceof PanelRepeaterGridItem) {
panelState = sceneUtils.cloneSceneObjectState(gridItem.state.source.state);
panelData = sceneGraph.getData(gridItem.state.source).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 SceneGridItem({
x: gridItem.state.x,
y: gridItem.state.y,
height: NEW_PANEL_HEIGHT,
width: NEW_PANEL_WIDTH,
body: new VizPanel({ ...panelState, $data: panelData, key: getVizPanelKeyForPanelId(newPanelId) }),
});
}
// when we duplicate a panel we don't want to clone the alert state
delete panelData.state.data?.alertState;
const { key: gridItemKey, ...gridItemToDuplicateState } = sceneUtils.cloneSceneObjectState(gridItem.state);
const newGridItem = new SceneGridItem({
...gridItemToDuplicateState,
body: new VizPanel({ ...panelState, $data: panelData }),
});
if (!(this.state.body instanceof SceneGridLayout)) {
console.error('Trying to duplicate a panel in a layout that is not SceneGridLayout ');
return;
@ -495,6 +516,18 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
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],
});

View File

@ -16,6 +16,7 @@ import { DashboardControls } from '../scene/DashboardControls';
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem';
import { dashboardSceneGraph, getNextPanelId } from './dashboardSceneGraph';
import { findVizPanelByKey } from './utils';
@ -123,7 +124,7 @@ describe('dashboardSceneGraph', () => {
expect(id).toBe(4);
});
it('should take library panels into account', () => {
it('should take library panels, panels in rows and panel repeaters into account', () => {
const scene = buildTestScene({
body: new SceneGridLayout({
children: [
@ -151,6 +152,17 @@ describe('dashboardSceneGraph', () => {
pluginId: 'table',
}),
}),
new PanelRepeaterGridItem({
source: new VizPanel({
title: 'Panel C',
key: 'panel-4',
pluginId: 'table',
}),
variableName: 'repeat',
repeatedPanels: [],
repeatDirection: 'h',
maxPerRow: 1,
}),
new SceneGridRow({
key: 'key',
title: 'row',
@ -178,7 +190,7 @@ describe('dashboardSceneGraph', () => {
const id = getNextPanelId(scene);
expect(id).toBe(4);
expect(id).toBe(5);
});
it('should get next panel id in a layout with rows', () => {

View File

@ -11,6 +11,7 @@ import {
import { DashboardScene } from '../scene/DashboardScene';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { VizPanelLinks } from '../scene/PanelLinks';
import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem';
import { getPanelIdForLibraryVizPanel, getPanelIdForVizPanel } from './utils';
@ -85,6 +86,21 @@ export function getNextPanelId(dashboard: DashboardScene): number {
}
for (const child of body.state.children) {
if (child instanceof PanelRepeaterGridItem) {
const vizPanel = child.state.source;
if (vizPanel) {
const panelId =
vizPanel instanceof LibraryVizPanel
? getPanelIdForLibraryVizPanel(vizPanel)
: getPanelIdForVizPanel(vizPanel);
if (panelId > max) {
max = panelId;
}
}
}
if (child instanceof SceneGridItem) {
const vizPanel = child.state.body;
@ -130,6 +146,19 @@ export function getNextPanelId(dashboard: DashboardScene): number {
return max + 1;
}
// Returns the LibraryVizPanel that corresponds to the given VizPanel if it exists
export const getLibraryVizPanelFromVizPanel = (vizPanel: VizPanel): LibraryVizPanel | null => {
if (vizPanel.parent instanceof LibraryVizPanel) {
return vizPanel.parent;
}
if (vizPanel.parent instanceof PanelRepeaterGridItem && vizPanel.parent.state.source instanceof LibraryVizPanel) {
return vizPanel.parent.state.source;
}
return null;
};
export const dashboardSceneGraph = {
getTimePicker,
getRefreshPicker,