diff --git a/public/app/features/dashboard-scene/scene/AddLibraryPanelWidget.test.tsx b/public/app/features/dashboard-scene/scene/AddLibraryPanelWidget.test.tsx new file mode 100644 index 00000000000..b982e2813ce --- /dev/null +++ b/public/app/features/dashboard-scene/scene/AddLibraryPanelWidget.test.tsx @@ -0,0 +1,252 @@ +import { SceneGridItem, SceneGridLayout, SceneGridRow, SceneTimeRange } from '@grafana/scenes'; +import { LibraryPanel } from '@grafana/schema/dist/esm/index.gen'; + +import { DashboardScene } from '../scene/DashboardScene'; +import { activateFullSceneTree } from '../utils/test-utils'; + +import { AddLibraryPanelWidget } from './AddLibraryPanelWidget'; +import { LibraryVizPanel } from './LibraryVizPanel'; + +describe('AddLibraryPanelWidget', () => { + let dashboard: DashboardScene; + let addLibPanelWidget: AddLibraryPanelWidget; + const mockEvent = { + preventDefault: jest.fn(), + } as unknown as React.MouseEvent; + + beforeEach(async () => { + const result = await buildTestScene(); + dashboard = result.dashboard; + addLibPanelWidget = result.addLibPanelWidget; + }); + + it('should return the dashboard', () => { + expect(addLibPanelWidget.getDashboard()).toBe(dashboard); + }); + + it('should cancel adding a lib panel', () => { + addLibPanelWidget.onCancelAddPanel(mockEvent); + + const body = dashboard.state.body as SceneGridLayout; + + expect(body.state.children.length).toBe(0); + }); + + it('should cancel lib panel at correct position', () => { + const anotherLibPanelWidget = new AddLibraryPanelWidget({ key: 'panel-2' }); + const body = dashboard.state.body as SceneGridLayout; + + body.setState({ + children: [ + ...body.state.children, + new SceneGridItem({ + key: 'griditem-2', + x: 0, + y: 0, + width: 10, + height: 12, + body: anotherLibPanelWidget, + }), + ], + }); + dashboard.setState({ body }); + + anotherLibPanelWidget.onCancelAddPanel(mockEvent); + + const gridItem = body.state.children[0] as SceneGridItem; + + expect(body.state.children.length).toBe(1); + expect(gridItem.state.body!.state.key).toBe(addLibPanelWidget.state.key); + }); + + it('should cancel lib panel inside a row child', () => { + const anotherLibPanelWidget = new AddLibraryPanelWidget({ key: 'panel-2' }); + dashboard.setState({ + body: new SceneGridLayout({ + children: [ + new SceneGridRow({ + key: 'panel-2', + children: [ + new SceneGridItem({ + key: 'griditem-2', + x: 0, + y: 0, + width: 10, + height: 12, + body: anotherLibPanelWidget, + }), + ], + }), + ], + }), + }); + + const body = dashboard.state.body as SceneGridLayout; + + anotherLibPanelWidget.onCancelAddPanel(mockEvent); + + const gridRow = body.state.children[0] as SceneGridRow; + + expect(body.state.children.length).toBe(1); + expect(gridRow.state.children.length).toBe(0); + }); + + it('should add library panel from menu', () => { + const panelInfo: LibraryPanel = { + uid: 'uid', + model: { + type: 'timeseries', + }, + name: 'name', + version: 1, + type: 'timeseries', + }; + + const body = dashboard.state.body as SceneGridLayout; + const gridItem = body.state.children[0] as SceneGridItem; + + expect(gridItem.state.body!).toBeInstanceOf(AddLibraryPanelWidget); + + addLibPanelWidget.onAddLibraryPanel(panelInfo); + + expect(body.state.children.length).toBe(1); + expect(gridItem.state.body!).toBeInstanceOf(LibraryVizPanel); + expect((gridItem.state.body! as LibraryVizPanel).state.panelKey).toBe(addLibPanelWidget.state.key); + }); + + it('should add a lib panel at correct position', () => { + const anotherLibPanelWidget = new AddLibraryPanelWidget({ key: 'panel-2' }); + const body = dashboard.state.body as SceneGridLayout; + + body.setState({ + children: [ + ...body.state.children, + new SceneGridItem({ + key: 'griditem-2', + x: 0, + y: 0, + width: 10, + height: 12, + body: anotherLibPanelWidget, + }), + ], + }); + dashboard.setState({ body }); + + const panelInfo: LibraryPanel = { + uid: 'uid', + model: { + type: 'timeseries', + }, + name: 'name', + version: 1, + type: 'timeseries', + }; + + anotherLibPanelWidget.onAddLibraryPanel(panelInfo); + + const gridItemOne = body.state.children[0] as SceneGridItem; + const gridItemTwo = body.state.children[1] as SceneGridItem; + + expect(body.state.children.length).toBe(2); + expect(gridItemOne.state.body!).toBeInstanceOf(AddLibraryPanelWidget); + expect((gridItemTwo.state.body! as LibraryVizPanel).state.panelKey).toBe(anotherLibPanelWidget.state.key); + }); + + it('should add library panel from menu to a row child', () => { + const anotherLibPanelWidget = new AddLibraryPanelWidget({ key: 'panel-2' }); + dashboard.setState({ + body: new SceneGridLayout({ + children: [ + new SceneGridRow({ + key: 'panel-2', + children: [ + new SceneGridItem({ + key: 'griditem-2', + x: 0, + y: 0, + width: 10, + height: 12, + body: anotherLibPanelWidget, + }), + ], + }), + ], + }), + }); + + const panelInfo: LibraryPanel = { + uid: 'uid', + model: { + type: 'timeseries', + }, + name: 'name', + version: 1, + type: 'timeseries', + }; + + const body = dashboard.state.body as SceneGridLayout; + + anotherLibPanelWidget.onAddLibraryPanel(panelInfo); + + const gridRow = body.state.children[0] as SceneGridRow; + const gridItem = gridRow.state.children[0] as SceneGridItem; + + expect(body.state.children.length).toBe(1); + expect(gridItem.state.body!).toBeInstanceOf(LibraryVizPanel); + expect((gridItem.state.body! as LibraryVizPanel).state.panelKey).toBe(anotherLibPanelWidget.state.key); + }); + + it('should throw error if adding lib panel in a layout that is not SceneGridLayout', () => { + dashboard.setState({ + body: undefined, + }); + + expect(() => addLibPanelWidget.onAddLibraryPanel({} as LibraryPanel)).toThrow( + 'Trying to add a library panel in a layout that is not SceneGridLayout' + ); + }); + + it('should throw error if removing the library panel widget in a layout that is not SceneGridLayout', () => { + dashboard.setState({ + body: undefined, + }); + + expect(() => addLibPanelWidget.onCancelAddPanel(mockEvent)).toThrow( + 'Trying to remove the library panel widget in a layout that is not SceneGridLayout' + ); + }); +}); + +async function buildTestScene() { + const addLibPanelWidget = new AddLibraryPanelWidget({ key: 'panel-1' }); + const dashboard = new DashboardScene({ + $timeRange: new SceneTimeRange({}), + title: 'hello', + uid: 'dash-1', + version: 4, + meta: { + canEdit: true, + }, + body: new SceneGridLayout({ + children: [ + new SceneGridItem({ + key: 'griditem-1', + x: 0, + y: 0, + width: 10, + height: 12, + body: addLibPanelWidget, + }), + ], + }), + }); + + activateFullSceneTree(dashboard); + + await new Promise((r) => setTimeout(r, 1)); + + dashboard.onEnterEditMode(); + + return { dashboard, addLibPanelWidget }; +} diff --git a/public/app/features/dashboard-scene/scene/AddLibraryPanelWidget.tsx b/public/app/features/dashboard-scene/scene/AddLibraryPanelWidget.tsx new file mode 100644 index 00000000000..3ff23786d25 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/AddLibraryPanelWidget.tsx @@ -0,0 +1,169 @@ +import { css, cx, keyframes } from '@emotion/css'; +import React from 'react'; +import tinycolor from 'tinycolor2'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { + SceneComponentProps, + SceneGridItem, + SceneGridLayout, + SceneGridRow, + SceneObjectBase, + SceneObjectState, +} from '@grafana/scenes'; +import { LibraryPanel } from '@grafana/schema'; +import { IconButton, useStyles2 } from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; +import { + LibraryPanelsSearch, + LibraryPanelsSearchVariant, +} from 'app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch'; + +import { getDashboardSceneFor } from '../utils/utils'; + +import { DashboardScene } from './DashboardScene'; +import { LibraryVizPanel } from './LibraryVizPanel'; + +export interface AddLibraryPanelWidgetState extends SceneObjectState { + key: string; +} + +export class AddLibraryPanelWidget extends SceneObjectBase { + public constructor(state: AddLibraryPanelWidgetState) { + super({ + ...state, + }); + } + + private get _dashboard(): DashboardScene { + return getDashboardSceneFor(this); + } + + public getDashboard(): DashboardScene { + return this._dashboard; + } + + public onCancelAddPanel = (evt: React.MouseEvent) => { + evt.preventDefault(); + + if (!(this._dashboard.state.body instanceof SceneGridLayout)) { + throw new Error('Trying to remove the library panel widget in a layout that is not SceneGridLayout'); + } + + const sceneGridLayout = this._dashboard.state.body; + const children = []; + + for (const child of sceneGridLayout.state.children) { + if (child.state.key !== this.parent?.state.key) { + children.push(child); + } + + if (child instanceof SceneGridRow) { + const rowChildren = []; + + for (const rowChild of child.state.children) { + if (rowChild instanceof SceneGridItem && rowChild.state.key !== this.parent?.state.key) { + rowChildren.push(rowChild); + } + } + + child.setState({ children: rowChildren }); + } + } + + sceneGridLayout.setState({ children }); + }; + + public onAddLibraryPanel = (panelInfo: LibraryPanel) => { + if (!(this._dashboard.state.body instanceof SceneGridLayout)) { + throw new Error('Trying to add a library panel in a layout that is not SceneGridLayout'); + } + + const body = new LibraryVizPanel({ + title: 'Panel Title', + uid: panelInfo.uid, + name: panelInfo.name, + panelKey: this.state.key, + }); + + if (this.parent instanceof SceneGridItem) { + this.parent.setState({ body }); + } + }; + + static Component = ({ model }: SceneComponentProps) => { + const dashboard = model.getDashboard(); + const styles = useStyles2(getStyles); + + return ( +
+
+
+ + Add panel from panel library + +
+ +
+ +
+
+ ); + }; +} + +const getStyles = (theme: GrafanaTheme2) => { + const pulsate = keyframes({ + '0%': { + boxShadow: `0 0 0 2px ${theme.colors.background.canvas}, 0 0 0px 4px ${theme.colors.primary.main}`, + }, + '50%': { + boxShadow: `0 0 0 2px ${theme.components.dashboard.background}, 0 0 0px 4px ${tinycolor(theme.colors.primary.main) + .darken(20) + .toHexString()}`, + }, + '100%': { + boxShadow: `0 0 0 2px ${theme.components.dashboard.background}, 0 0 0px 4px ${theme.colors.primary.main}`, + }, + }); + + return { + // wrapper is used to make sure box-shadow animation isn't cut off in dashboard page + wrapper: css({ + height: '100%', + paddingTop: `${theme.spacing(0.5)}`, + }), + headerRow: css({ + display: 'flex', + alignItems: 'center', + height: '38px', + flexShrink: 0, + width: '100%', + fontSize: theme.typography.fontSize, + fontWeight: theme.typography.fontWeightMedium, + paddingLeft: `${theme.spacing(1)}`, + transition: 'background-color 0.1s ease-in-out', + cursor: 'move', + + '&:hover': { + background: `${theme.colors.background.secondary}`, + }, + }), + callToAction: css({ + overflow: 'hidden', + outline: '2px dotted transparent', + outlineOffset: '2px', + boxShadow: '0 0 0 2px black, 0 0 0px 4px #1f60c4', + animation: `${pulsate} 2s ease infinite`, + }), + }; +}; diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 045ef0799bf..0b135657229 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -264,6 +264,17 @@ describe('DashboardScene', () => { expect(gridItem.state.y).toBe(0); expect(scene.state.hasCopiedPanel).toBe(false); }); + + it('Should create a new add library panel widget', () => { + scene.onCreateLibPanelWidget(); + + const body = scene.state.body as SceneGridLayout; + const gridItem = body.state.children[0] as SceneGridItem; + + expect(body.state.children.length).toBe(5); + expect(gridItem.state.body!.state.key).toBe('panel-5'); + expect(gridItem.state.y).toBe(0); + }); }); }); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 5bfbe60a68c..a1a4059e093 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -59,6 +59,7 @@ import { isPanelClone, } from '../utils/utils'; +import { AddLibraryPanelWidget } from './AddLibraryPanelWidget'; import { DashboardControls } from './DashboardControls'; import { DashboardSceneUrlSync } from './DashboardSceneUrlSync'; import { PanelRepeaterGridItem } from './PanelRepeaterGridItem'; @@ -612,6 +613,29 @@ export class DashboardScene extends SceneObjectBase { locationService.partial({ editview: 'settings' }); }; + public onCreateLibPanelWidget() { + if (!(this.state.body instanceof SceneGridLayout)) { + throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); + } + + const sceneGridLayout = this.state.body; + + const panelId = dashboardSceneGraph.getNextPanelId(this); + + const newGridItem = new SceneGridItem({ + height: NEW_PANEL_HEIGHT, + width: NEW_PANEL_WIDTH, + x: 0, + y: 0, + body: new AddLibraryPanelWidget({ key: getVizPanelKeyForPanelId(panelId) }), + key: `grid-item-${panelId}`, + }); + + sceneGridLayout.setState({ + children: [newGridItem, ...sceneGridLayout.state.children], + }); + } + public onCreateNewRow() { const row = getDefaultRow(this); diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx index 0c192c1df9a..da1c138fb79 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx @@ -18,6 +18,7 @@ describe('NavToolbarActions', () => { expect(screen.queryByLabelText('Add visualization')).not.toBeInTheDocument(); expect(screen.queryByLabelText('Add row')).not.toBeInTheDocument(); expect(screen.queryByLabelText('Paste panel')).not.toBeInTheDocument(); + expect(screen.queryByLabelText('Add library panel')).not.toBeInTheDocument(); expect(await screen.findByText('Edit')).toBeInTheDocument(); expect(await screen.findByText('Share')).toBeInTheDocument(); }); @@ -32,6 +33,7 @@ describe('NavToolbarActions', () => { expect(await screen.findByLabelText('Add visualization')).toBeInTheDocument(); expect(await screen.findByLabelText('Add row')).toBeInTheDocument(); expect(await screen.findByLabelText('Paste panel')).toBeInTheDocument(); + expect(await screen.findByLabelText('Add library panel')).toBeInTheDocument(); expect(screen.queryByText('Edit')).not.toBeInTheDocument(); expect(screen.queryByText('Share')).not.toBeInTheDocument(); }); diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 76215619b74..232189129fc 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -71,6 +71,22 @@ export function ToolbarActions({ dashboard }: Props) { ), }); + toolbarActions.push({ + group: 'icon-actions', + condition: isEditing && !editview && !isViewingPanel && !isEditingPanel, + render: () => ( + { + dashboard.onCreateLibPanelWidget(); + DashboardInteractions.toolbarAddButtonClicked({ item: 'add_library_panel' }); + }} + /> + ), + }); + toolbarActions.push({ group: 'icon-actions', condition: isEditing && !editview && !isViewingPanel && !isEditingPanel, diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index 3992895e622..908dbbcca4c 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -42,11 +42,13 @@ import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types'; import { DashboardDataDTO } from 'app/types'; +import { AddLibraryPanelWidget } from '../scene/AddLibraryPanelWidget'; +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; import { PanelTimeRange } from '../scene/PanelTimeRange'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { NEW_LINK } from '../settings/links/utils'; -import { getQueryRunnerFor } from '../utils/utils'; +import { getQueryRunnerFor, getVizPanelKeyForPanelId } from '../utils/utils'; import { buildNewDashboardSaveModel } from './buildNewDashboardSaveModel'; import { GRAFANA_DATASOURCE_REF } from './const'; @@ -58,6 +60,8 @@ import { createSceneVariableFromVariableModel, transformSaveModelToScene, convertOldSnapshotToScenesSnapshot, + buildGridItemForLibPanel, + buildGridItemForLibraryPanelWidget, } from './transformSaveModelToScene'; describe('transformSaveModelToScene', () => { @@ -453,6 +457,37 @@ describe('transformSaveModelToScene', () => { expect(runner.state.cacheTimeout).toBe('10'); expect(runner.state.queryCachingTTL).toBe(200000); }); + it('should convert saved lib widget to AddLibraryPanelWidget', () => { + const panel = { + id: 10, + type: 'add-library-panel', + }; + + const gridItem = buildGridItemForLibraryPanelWidget(new PanelModel(panel))!; + const libPanelWidget = gridItem.state.body as AddLibraryPanelWidget; + + expect(libPanelWidget.state.key).toEqual(getVizPanelKeyForPanelId(panel.id)); + }); + + it('should convert saved lib panel to LibraryVizPanel', () => { + const panel = { + title: 'Panel', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + transparent: true, + libraryPanel: { + uid: '123', + name: 'My Panel', + folderUid: '456', + }, + }; + + const gridItem = buildGridItemForLibPanel(new PanelModel(panel))!; + const libVizPanel = gridItem.state.body as LibraryVizPanel; + + expect(libVizPanel.state.uid).toEqual(panel.libraryPanel.uid); + expect(libVizPanel.state.name).toEqual(panel.libraryPanel.name); + expect(libVizPanel.state.title).toEqual(panel.title); + }); }); describe('when creating variables objects', () => { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 096f378a018..1957b83104c 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -34,6 +34,7 @@ import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { trackDashboardLoaded } from 'app/features/dashboard/utils/tracking'; import { DashboardDTO } from 'app/types'; +import { AddLibraryPanelWidget } from '../scene/AddLibraryPanelWidget'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardControls } from '../scene/DashboardControls'; @@ -110,6 +111,12 @@ export function createSceneObjectsForPanels(oldPanels: PanelModel[]): SceneGridI currentRowPanels = []; } } + } else if (panel.type === 'add-library-panel') { + const gridItem = buildGridItemForLibraryPanelWidget(panel); + + if (gridItem) { + panels.push(gridItem); + } } else if (panel.libraryPanel?.uid && !('model' in panel.libraryPanel)) { const gridItem = buildGridItemForLibPanel(panel); if (gridItem) { @@ -399,6 +406,24 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode } } +export function buildGridItemForLibraryPanelWidget(panel: PanelModel) { + if (panel.type !== 'add-library-panel') { + return null; + } + + const body = new AddLibraryPanelWidget({ + key: getVizPanelKeyForPanelId(panel.id), + }); + + return new SceneGridItem({ + body, + y: panel.gridPos.y, + x: panel.gridPos.x, + width: panel.gridPos.w, + height: panel.gridPos.h, + }); +} + export function buildGridItemForLibPanel(panel: PanelModel) { if (!panel.libraryPanel) { return null; diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts index 2901b087238..f088fc622c6 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts @@ -41,6 +41,7 @@ import snapshotableDashboardJson from './testfiles/snapshotable_dashboard.json'; import snapshotableWithRowsDashboardJson from './testfiles/snapshotable_with_rows.json'; import { buildGridItemForLibPanel, + buildGridItemForLibraryPanelWidget, buildGridItemForPanel, transformSaveModelToScene, } from './transformSaveModelToScene'; @@ -351,6 +352,30 @@ describe('transformSceneToSaveModel', () => { expect(result.transformations).toBeUndefined(); expect(result.fieldConfig).toBeUndefined(); }); + + it('given a library panel widget', () => { + const panel = buildGridItemFromPanelSchema({ + id: 4, + gridPos: { + h: 8, + w: 12, + x: 0, + y: 0, + }, + type: 'add-library-panel', + }); + + const result = gridItemToPanel(panel); + + expect(result.id).toBe(4); + expect(result.gridPos).toEqual({ + h: 8, + w: 12, + x: 0, + y: 0, + }); + expect(result.type).toBe('add-library-panel'); + }); }); describe('Annotations', () => { @@ -897,6 +922,9 @@ describe('transformSceneToSaveModel', () => { export function buildGridItemFromPanelSchema(panel: Partial): SceneGridItemLike { if (panel.libraryPanel) { return buildGridItemForLibPanel(new PanelModel(panel))!; + } else if (panel.type === 'add-library-panel') { + return buildGridItemForLibraryPanelWidget(new PanelModel(panel))!; } + return buildGridItemForPanel(new PanelModel(panel)); } diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index d0f61b9f2cf..12028b43cdc 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -33,6 +33,7 @@ import { getPanelDataFrames } from 'app/features/dashboard/components/HelpWizard import { DASHBOARD_SCHEMA_VERSION } from 'app/features/dashboard/state/DashboardMigrator'; import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types'; +import { AddLibraryPanelWidget } from '../scene/AddLibraryPanelWidget'; import { DashboardScene } from '../scene/DashboardScene'; import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; @@ -167,6 +168,20 @@ export function gridItemToPanel(gridItem: SceneGridItemLike, isSnapshot = false) } as Panel; } + // Handle library panel widget as well and exit early + if (gridItem.state.body instanceof AddLibraryPanelWidget) { + x = gridItem.state.x ?? 0; + y = gridItem.state.y ?? 0; + w = gridItem.state.width ?? 0; + h = gridItem.state.height ?? 0; + + return { + id: getPanelIdForVizPanel(gridItem.state.body), + type: 'add-library-panel', + gridPos: { x, y, w, h }, + }; + } + if (!(gridItem.state.body instanceof VizPanel)) { throw new Error('SceneGridItem body expected to be VizPanel'); }