Scenes: Add support for repeated library panels (#83579)

This commit is contained in:
kay delaney 2024-03-01 13:25:15 +00:00 committed by GitHub
parent dbe621eeb4
commit 10a55560df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 204 additions and 10 deletions

View File

@ -2573,7 +2573,8 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"]
[0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Do not use any type assertions.", "7"]
],
"public/app/features/dashboard-scene/settings/variables/components/VariableSelectField.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]

View File

@ -0,0 +1,136 @@
import 'whatwg-fetch';
import { waitFor } from '@testing-library/dom';
import { merge } from 'lodash';
import { http, HttpResponse } from 'msw';
import { setupServer, SetupServerApi } from 'msw/node';
import { setBackendSrv } from '@grafana/runtime';
import { SceneGridItem, SceneGridLayout, VizPanel } from '@grafana/scenes';
import { LibraryPanel } from '@grafana/schema';
import { backendSrv } from 'app/core/services/backend_srv';
import { LibraryVizPanel } from './LibraryVizPanel';
import { PanelRepeaterGridItem } from './PanelRepeaterGridItem';
describe('LibraryVizPanel', () => {
const server = setupServer();
beforeAll(() => {
setBackendSrv(backendSrv);
server.listen();
});
afterAll(() => {
server.close();
});
beforeEach(() => {
server.resetHandlers();
});
it('should fetch and init', async () => {
setUpApiMock(server);
const libVizPanel = new LibraryVizPanel({
name: 'My Library Panel',
title: 'Panel title',
uid: 'fdcvggvfy2qdca',
panelKey: 'lib-panel',
});
libVizPanel.activate();
await waitFor(() => {
expect(libVizPanel.state.panel).toBeInstanceOf(VizPanel);
});
});
it('should change parent from SceneGridItem to PanelRepeaterGridItem if repeat is set', async () => {
setUpApiMock(server, { model: { repeat: 'query0', repeatDirection: 'h' } });
const libVizPanel = new LibraryVizPanel({
name: 'My Library Panel',
title: 'Panel title',
uid: 'fdcvggvfy2qdca',
panelKey: 'lib-panel',
});
const layout = new SceneGridLayout({
children: [new SceneGridItem({ body: libVizPanel })],
});
layout.activate();
libVizPanel.activate();
await waitFor(() => {
expect(layout.state.children[0]).toBeInstanceOf(PanelRepeaterGridItem);
});
});
});
function setUpApiMock(
server: SetupServerApi,
overrides: Omit<Partial<LibraryPanel>, 'model'> & { model?: Partial<LibraryPanel['model']> } = {}
) {
const libPanel: LibraryPanel = merge(
{
folderUid: 'general',
uid: 'fdcvggvfy2qdca',
name: 'My Library Panel',
type: 'timeseries',
description: '',
model: {
datasource: {
type: 'grafana-testdata-datasource',
uid: 'PD8C576611E62080A',
},
description: '',
maxPerRow: 4,
options: {
legend: {
calcs: [],
displayMode: 'list',
placement: 'bottom',
showLegend: true,
},
tooltip: {
maxHeight: 600,
mode: 'single',
sort: 'none',
},
},
targets: [
{
datasource: {
type: 'grafana-testdata-datasource',
uid: 'PD8C576611E62080A',
},
refId: 'A',
},
],
title: 'Panel Title',
type: 'timeseries',
},
version: 6,
meta: {
folderName: 'General',
folderUid: '',
connectedDashboards: 1,
created: '2024-02-15T15:26:46Z',
updated: '2024-02-28T15:54:22Z',
createdBy: {
avatarUrl: '/avatar/46d229b033af06a191ff2267bca9ae56',
id: 1,
name: 'admin',
},
updatedBy: {
avatarUrl: '/avatar/46d229b033af06a191ff2267bca9ae56',
id: 1,
name: 'admin',
},
},
},
overrides
);
const libPanelMock: { result: LibraryPanel } = {
result: libPanel,
};
server.use(http.get('/api/library-elements/:uid', () => HttpResponse.json(libPanelMock)));
}

View File

@ -1,6 +1,15 @@
import React from 'react';
import { SceneComponentProps, SceneObjectBase, SceneObjectState, VizPanel, VizPanelMenu } from '@grafana/scenes';
import {
SceneComponentProps,
SceneGridItem,
SceneGridLayout,
SceneObjectBase,
SceneObjectState,
VizPanel,
VizPanelMenu,
VizPanelState,
} from '@grafana/scenes';
import { PanelModel } from 'app/features/dashboard/state';
import { getLibraryPanel } from 'app/features/library-panels/state/api';
@ -9,6 +18,7 @@ import { createPanelDataProvider } from '../utils/createPanelDataProvider';
import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks';
import { panelLinksBehavior, panelMenuBehavior } from './PanelMenuBehavior';
import { PanelNotices } from './PanelNotices';
import { PanelRepeaterGridItem } from './PanelRepeaterGridItem';
interface LibraryVizPanelState extends SceneObjectState {
// Library panels use title from dashboard JSON's panel model, not from library panel definition, hence we pass it.
@ -52,7 +62,7 @@ export class LibraryVizPanel extends SceneObjectBase<LibraryVizPanelState> {
const libPanelModel = new PanelModel(libPanel.model);
const panel = new VizPanel({
const vizPanelState: VizPanelState = {
title: this.state.title,
key: this.state.panelKey,
options: libPanelModel.options ?? {},
@ -70,12 +80,36 @@ export class LibraryVizPanel extends SceneObjectBase<LibraryVizPanelState> {
}),
new PanelNotices(),
],
});
};
const panel = new VizPanel(vizPanelState);
const gridItem = this.parent;
if (libPanelModel.repeat && gridItem instanceof SceneGridItem && gridItem.parent instanceof SceneGridLayout) {
this._parent = undefined;
const repeater = new PanelRepeaterGridItem({
key: gridItem.state.key,
x: libPanelModel.gridPos.x,
y: libPanelModel.gridPos.y,
width: libPanelModel.repeatDirection === 'h' ? 24 : libPanelModel.gridPos.w,
height: libPanelModel.gridPos.h,
itemHeight: libPanelModel.gridPos.h,
source: this,
variableName: libPanelModel.repeat,
repeatedPanels: [],
repeatDirection: libPanelModel.repeatDirection === 'h' ? 'h' : 'v',
maxPerRow: libPanelModel.maxPerRow,
});
gridItem.parent.setState({
children: gridItem.parent.state.children.map((child) =>
child.state.key === gridItem.state.key ? repeater : child
),
});
}
this.setState({ panel, _loadedVersion: libPanel.version, isLoaded: true });
} catch (err) {
vizPanel.setState({
_pluginLoadError: 'Unable to load library panel: ' + this.state.uid,
_pluginLoadError: `Unable to load library panel: ${this.state.uid}`,
});
}
}

View File

@ -19,10 +19,11 @@ import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
import { getMultiVariableValues } from '../utils/utils';
import { LibraryVizPanel } from './LibraryVizPanel';
import { DashboardRepeatsProcessedEvent } from './types';
interface PanelRepeaterGridItemState extends SceneGridItemStateLike {
source: VizPanel;
source: VizPanel | LibraryVizPanel;
repeatedPanels?: VizPanel[];
variableName: string;
itemHeight?: number;
@ -94,11 +95,12 @@ export class PanelRepeaterGridItem extends SceneObjectBase<PanelRepeaterGridItem
return;
}
const panelToRepeat = this.state.source;
let panelToRepeat =
this.state.source instanceof LibraryVizPanel ? this.state.source.state.panel! : this.state.source;
const { values, texts } = getMultiVariableValues(variable);
const repeatedPanels: VizPanel[] = [];
// Loop through variable values and create repeates
// Loop through variable values and create repeats
for (let index = 0; index < values.length; index++) {
const clone = panelToRepeat.clone({
$variables: new SceneVariableSet({

View File

@ -194,7 +194,12 @@ export function gridItemToPanel(gridItem: SceneGridItemLike, isSnapshot = false)
}
if (gridItem instanceof PanelRepeaterGridItem) {
vizPanel = gridItem.state.source;
if (gridItem.state.source instanceof LibraryVizPanel) {
vizPanel = gridItem.state.source.state.panel;
} else {
vizPanel = gridItem.state.source;
}
x = gridItem.state.x ?? 0;
y = gridItem.state.y ?? 0;
w = gridItem.state.width ?? 0;
@ -316,6 +321,22 @@ export function panelRepeaterToPanels(repeater: PanelRepeaterGridItem, isSnapsho
if (!isSnapshot) {
return [gridItemToPanel(repeater)];
} else {
if (repeater.state.source instanceof LibraryVizPanel) {
const { x = 0, y = 0, width: w = 0, height: h = 0 } = repeater.state;
return [
{
id: getPanelIdForVizPanel(repeater.state.source),
title: repeater.state.source.state.title,
gridPos: { x, y, w, h },
libraryPanel: {
name: repeater.state.source.state.name,
uid: repeater.state.source.state.uid,
},
} as Panel,
];
}
if (repeater.state.repeatedPanels) {
const itemHeight = repeater.state.itemHeight ?? 10;
const rowCount = Math.ceil(repeater.state.repeatedPanels!.length / repeater.getMaxPerRow());

View File

@ -65,7 +65,7 @@ function rowTypes(gridRow: SceneGridRow) {
}
function panelDatasourceTypes(gridItem: SceneGridItemLike) {
let vizPanel: VizPanel | undefined;
let vizPanel: VizPanel | LibraryVizPanel | undefined;
if (gridItem instanceof SceneGridItem) {
if (gridItem.state.body instanceof LibraryVizPanel) {
vizPanel = gridItem.state.body.state.panel;