From 10a55560dfc09a63aa4a7ffe8bc00618abc1c935 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Fri, 1 Mar 2024 13:25:15 +0000 Subject: [PATCH] Scenes: Add support for repeated library panels (#83579) --- .betterer.results | 3 +- .../scene/LibraryVizPanel.test.ts | 136 ++++++++++++++++++ .../dashboard-scene/scene/LibraryVizPanel.tsx | 42 +++++- .../scene/PanelRepeaterGridItem.tsx | 8 +- .../transformSceneToSaveModel.ts | 23 ++- .../sharing/public-dashboards/utils.ts | 2 +- 6 files changed, 204 insertions(+), 10 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/LibraryVizPanel.test.ts diff --git a/.betterer.results b/.betterer.results index 48f308f57b5..d89b65b7bbf 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"] diff --git a/public/app/features/dashboard-scene/scene/LibraryVizPanel.test.ts b/public/app/features/dashboard-scene/scene/LibraryVizPanel.test.ts new file mode 100644 index 00000000000..39968af0702 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/LibraryVizPanel.test.ts @@ -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, 'model'> & { model?: Partial } = {} +) { + 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))); +} diff --git a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx b/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx index b1c2be5d6fa..379a5ba1b4a 100644 --- a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx @@ -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 { 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 { }), 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}`, }); } } diff --git a/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx b/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx index ccaa1573604..631ecda727d 100644 --- a/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx @@ -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