diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx index 903ce6daddc..0ba2851eaea 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx @@ -9,10 +9,13 @@ import { SceneGridLayout, VizPanel, } from '@grafana/scenes'; +import * as libpanels from 'app/features/library-panels/state/api'; import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; import { DashboardScene } from '../scene/DashboardScene'; +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; +import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel'; import { activateFullSceneTree } from '../utils/test-utils'; import { findVizPanelByKey } from '../utils/utils'; @@ -25,6 +28,14 @@ setPluginImportUtils({ getPanelPluginFromCache: (id: string) => undefined, }); +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + setPluginExtensionGetter: jest.fn(), + getPluginLinkExtensions: jest.fn(() => ({ + extensions: [], + })), +})); + describe('InspectJsonTab', () => { it('Can show panel json', async () => { const { tab } = await buildTestScene(); @@ -34,6 +45,15 @@ describe('InspectJsonTab', () => { expect(tab.isEditable()).toBe(true); }); + it('Can show panel json for library panels', async () => { + const { tab } = await buildTestSceneWithLibraryPanel(); + + const obj = JSON.parse(tab.state.jsonText); + expect(obj.gridPos).toEqual({ x: 0, y: 0, w: 10, h: 12 }); + expect(obj.type).toEqual('table'); + expect(tab.isEditable()).toBe(false); + }); + it('Can show panel data with field config', async () => { const { tab } = await buildTestScene(); tab.onChangeSource({ value: 'panel-data' }); @@ -86,8 +106,8 @@ describe('InspectJsonTab', () => { }); }); -async function buildTestScene() { - const panel = new VizPanel({ +function buildTestPanel() { + return new VizPanel({ title: 'Panel A', pluginId: 'table', key: 'panel-12', @@ -129,7 +149,10 @@ async function buildTestScene() { }), }), }); +} +async function buildTestScene() { + const panel = buildTestPanel(); const scene = new DashboardScene({ title: 'hello', uid: 'dash-1', @@ -161,3 +184,50 @@ async function buildTestScene() { return { scene, tab, panel }; } + +async function buildTestSceneWithLibraryPanel() { + const panel = vizPanelToPanel(buildTestPanel()); + + const libraryPanelState = { + name: 'LibraryPanel A', + title: 'LibraryPanel A title', + uid: '111', + key: 'panel-22', + model: panel, + version: 1, + }; + + jest.spyOn(libpanels, 'getLibraryPanel').mockResolvedValue({ ...libraryPanelState, ...panel }); + const libraryPanel = new LibraryVizPanel(libraryPanelState); + + const scene = new DashboardScene({ + title: 'hello', + uid: 'dash-1', + meta: { + canEdit: true, + }, + body: new SceneGridLayout({ + children: [ + new SceneGridItem({ + key: 'griditem-1', + x: 0, + y: 0, + width: 10, + height: 12, + body: libraryPanel, + }), + ], + }), + }); + + activateFullSceneTree(scene); + + await new Promise((r) => setTimeout(r, 1)); + + const tab = new InspectJsonTab({ + panelRef: libraryPanel.state.panel!.getRef(), + onClose: jest.fn(), + }); + + return { scene, tab, panel }; +} diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx index 0b3d2ac23c2..f8abf99f087 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx @@ -26,9 +26,10 @@ import { InspectTab } from 'app/features/inspector/types'; import { getPrettyJSON } from 'app/features/inspector/utils/utils'; import { reportPanelInspectInteraction } from 'app/features/search/page/reporting'; +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; import { buildGridItemForPanel } from '../serialization/transformSaveModelToScene'; -import { gridItemToPanel } from '../serialization/transformSceneToSaveModel'; +import { gridItemToPanel, vizPanelToPanel } from '../serialization/transformSceneToSaveModel'; import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils'; export type ShowContent = 'panel-json' | 'panel-data' | 'data-frames'; @@ -202,6 +203,8 @@ function getJsonText(show: ShowContent, panel: VizPanel): string { if (panel.parent instanceof SceneGridItem || panel.parent instanceof PanelRepeaterGridItem) { objToStringify = gridItemToPanel(panel.parent); + } else if (panel.parent instanceof LibraryVizPanel) { + objToStringify = libraryPanelChildToLegacyRepresentation(panel); } break; } @@ -234,6 +237,30 @@ function getJsonText(show: ShowContent, panel: VizPanel): string { return getPrettyJSON(objToStringify); } +/** + * + * @param panel Must be child of a LibraryVizPanel that is in turn the child of a SceneGridItem + * @returns object representation of the legacy library panel structure. + */ +function libraryPanelChildToLegacyRepresentation(panel: VizPanel<{}, {}>) { + if (!(panel.parent instanceof LibraryVizPanel)) { + throw 'Panel not child of LibraryVizPanel'; + } + if (!(panel.parent.parent instanceof SceneGridItem)) { + throw 'LibraryPanel not child of SceneGridItem'; + } + const gridItem = panel.parent.parent; + const libraryPanelObj = gridItemToPanel(gridItem); + const panelObj = vizPanelToPanel(panel); + panelObj.gridPos = { + x: gridItem.state.x || 0, + y: gridItem.state.y || 0, + h: gridItem.state.height || 0, + w: gridItem.state.width || 0, + }; + return { ...libraryPanelObj, ...panelObj }; +} + function hasGridPosChanged(a: SceneGridItemStateLike, b: SceneGridItemStateLike) { return a.x !== b.x || a.y !== b.y || a.width !== b.width || a.height !== b.height; } diff --git a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx b/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx index c7ae879fe60..61beed8fff2 100644 --- a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx @@ -6,31 +6,26 @@ import { getLibraryPanel } from 'app/features/library-panels/state/api'; import { createPanelDataProvider } from '../utils/createPanelDataProvider'; -import { panelMenuBehavior } from './PanelMenuBehavior'; +import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks'; +import { panelLinksBehavior, panelMenuBehavior } from './PanelMenuBehavior'; +import { PanelNotices } from './PanelNotices'; interface LibraryVizPanelState extends SceneObjectState { // Library panels use title from dashboard JSON's panel model, not from library panel definition, hence we pass it. title: string; uid: string; name: string; - panel: VizPanel; + panel?: VizPanel; + _loadedVersion?: number; } export class LibraryVizPanel extends SceneObjectBase { static Component = LibraryPanelRenderer; - constructor({ uid, title, key, name }: Pick) { + constructor(state: LibraryVizPanelState) { super({ - uid, - title, - key, - name, - panel: new VizPanel({ - title, - menu: new VizPanelMenu({ - $behaviors: [panelMenuBehavior], - }), - }), + panel: state.panel ?? getLoadingPanel(state.title), + ...state, }); this.addActivationHandler(this._onActivate); @@ -41,29 +36,54 @@ export class LibraryVizPanel extends SceneObjectBase { }; private async loadLibraryPanelFromPanelModel() { - let vizPanel = this.state.panel; + let vizPanel = this.state.panel!; + try { const libPanel = await getLibraryPanel(this.state.uid, true); + + if (this.state._loadedVersion === libPanel.version) { + return; + } + const libPanelModel = new PanelModel(libPanel.model); - vizPanel = vizPanel.clone({ + + const panel = new VizPanel({ + title: this.state.title, options: libPanelModel.options ?? {}, fieldConfig: libPanelModel.fieldConfig, + pluginId: libPanelModel.type, pluginVersion: libPanelModel.pluginVersion, displayMode: libPanelModel.transparent ? 'transparent' : undefined, description: libPanelModel.description, - pluginId: libPanel.type, $data: createPanelDataProvider(libPanelModel), + menu: new VizPanelMenu({ $behaviors: [panelMenuBehavior] }), + titleItems: [ + new VizPanelLinks({ + rawLinks: libPanelModel.links, + menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }), + }), + new PanelNotices(), + ], }); + + this.setState({ panel, _loadedVersion: libPanel.version }); } catch (err) { vizPanel.setState({ _pluginLoadError: 'Unable to load library panel: ' + this.state.uid, }); } - - this.setState({ panel: vizPanel }); } } +function getLoadingPanel(title: string) { + return new VizPanel({ + title, + menu: new VizPanelMenu({ + $behaviors: [panelMenuBehavior], + }), + }); +} + function LibraryPanelRenderer({ model }: SceneComponentProps) { const { panel } = model.useState(); diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index e6301ac4ef7..b47bd847fd4 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -22,6 +22,7 @@ import { defaultDashboard, defaultTimePickerConfig, FieldConfigSource, + GridPos, Panel, RowPanel, TimePickerConfig, @@ -200,11 +201,22 @@ export function gridItemToPanel(gridItem: SceneGridItemLike, isSnapshot = false) throw new Error('Unsupported grid item type'); } + const panel: Panel = vizPanelToPanel(vizPanel, { x, y, h, w }, isSnapshot, gridItem); + + return panel; +} + +export function vizPanelToPanel( + vizPanel: VizPanel, + gridPos?: GridPos, + isSnapshot = false, + gridItem?: SceneGridItemLike +) { const panel: Panel = { id: getPanelIdForVizPanel(vizPanel), type: vizPanel.state.pluginId, title: vizPanel.state.title, - gridPos: { x, y, w, h }, + gridPos, options: vizPanel.state.options, fieldConfig: (vizPanel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] }, transformations: [], @@ -241,7 +253,6 @@ export function gridItemToPanel(gridItem: SceneGridItemLike, isSnapshot = false) if (!panel.transparent) { delete panel.transparent; } - return panel; }