Dashboard scenes: Fixes inspect library panels (#82879)

* working except for links

* Make sure the links are present on the library panels

* add tests, add empty links before panel is loaded, refactor legacy representation

* Update

---------

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
Oscar Kilhed 2024-02-19 15:25:45 +01:00 committed by GitHub
parent 1f98028962
commit b56f6ed0dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 151 additions and 23 deletions

View File

@ -9,10 +9,13 @@ import {
SceneGridLayout, SceneGridLayout,
VizPanel, VizPanel,
} from '@grafana/scenes'; } from '@grafana/scenes';
import * as libpanels from 'app/features/library-panels/state/api';
import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; import { getStandardTransformers } from 'app/features/transformers/standardTransformers';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
import { activateFullSceneTree } from '../utils/test-utils'; import { activateFullSceneTree } from '../utils/test-utils';
import { findVizPanelByKey } from '../utils/utils'; import { findVizPanelByKey } from '../utils/utils';
@ -25,6 +28,14 @@ setPluginImportUtils({
getPanelPluginFromCache: (id: string) => undefined, getPanelPluginFromCache: (id: string) => undefined,
}); });
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
setPluginExtensionGetter: jest.fn(),
getPluginLinkExtensions: jest.fn(() => ({
extensions: [],
})),
}));
describe('InspectJsonTab', () => { describe('InspectJsonTab', () => {
it('Can show panel json', async () => { it('Can show panel json', async () => {
const { tab } = await buildTestScene(); const { tab } = await buildTestScene();
@ -34,6 +45,15 @@ describe('InspectJsonTab', () => {
expect(tab.isEditable()).toBe(true); 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 () => { it('Can show panel data with field config', async () => {
const { tab } = await buildTestScene(); const { tab } = await buildTestScene();
tab.onChangeSource({ value: 'panel-data' }); tab.onChangeSource({ value: 'panel-data' });
@ -86,8 +106,8 @@ describe('InspectJsonTab', () => {
}); });
}); });
async function buildTestScene() { function buildTestPanel() {
const panel = new VizPanel({ return new VizPanel({
title: 'Panel A', title: 'Panel A',
pluginId: 'table', pluginId: 'table',
key: 'panel-12', key: 'panel-12',
@ -129,7 +149,10 @@ async function buildTestScene() {
}), }),
}), }),
}); });
}
async function buildTestScene() {
const panel = buildTestPanel();
const scene = new DashboardScene({ const scene = new DashboardScene({
title: 'hello', title: 'hello',
uid: 'dash-1', uid: 'dash-1',
@ -161,3 +184,50 @@ async function buildTestScene() {
return { scene, tab, panel }; 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 };
}

View File

@ -26,9 +26,10 @@ import { InspectTab } from 'app/features/inspector/types';
import { getPrettyJSON } from 'app/features/inspector/utils/utils'; import { getPrettyJSON } from 'app/features/inspector/utils/utils';
import { reportPanelInspectInteraction } from 'app/features/search/page/reporting'; import { reportPanelInspectInteraction } from 'app/features/search/page/reporting';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem';
import { buildGridItemForPanel } from '../serialization/transformSaveModelToScene'; import { buildGridItemForPanel } from '../serialization/transformSaveModelToScene';
import { gridItemToPanel } from '../serialization/transformSceneToSaveModel'; import { gridItemToPanel, vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils'; import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
export type ShowContent = 'panel-json' | 'panel-data' | 'data-frames'; 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) { if (panel.parent instanceof SceneGridItem || panel.parent instanceof PanelRepeaterGridItem) {
objToStringify = gridItemToPanel(panel.parent); objToStringify = gridItemToPanel(panel.parent);
} else if (panel.parent instanceof LibraryVizPanel) {
objToStringify = libraryPanelChildToLegacyRepresentation(panel);
} }
break; break;
} }
@ -234,6 +237,30 @@ function getJsonText(show: ShowContent, panel: VizPanel): string {
return getPrettyJSON(objToStringify); 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) { function hasGridPosChanged(a: SceneGridItemStateLike, b: SceneGridItemStateLike) {
return a.x !== b.x || a.y !== b.y || a.width !== b.width || a.height !== b.height; return a.x !== b.x || a.y !== b.y || a.width !== b.width || a.height !== b.height;
} }

View File

@ -6,31 +6,26 @@ import { getLibraryPanel } from 'app/features/library-panels/state/api';
import { createPanelDataProvider } from '../utils/createPanelDataProvider'; 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 { interface LibraryVizPanelState extends SceneObjectState {
// Library panels use title from dashboard JSON's panel model, not from library panel definition, hence we pass it. // Library panels use title from dashboard JSON's panel model, not from library panel definition, hence we pass it.
title: string; title: string;
uid: string; uid: string;
name: string; name: string;
panel: VizPanel; panel?: VizPanel;
_loadedVersion?: number;
} }
export class LibraryVizPanel extends SceneObjectBase<LibraryVizPanelState> { export class LibraryVizPanel extends SceneObjectBase<LibraryVizPanelState> {
static Component = LibraryPanelRenderer; static Component = LibraryPanelRenderer;
constructor({ uid, title, key, name }: Pick<LibraryVizPanelState, 'uid' | 'title' | 'key' | 'name'>) { constructor(state: LibraryVizPanelState) {
super({ super({
uid, panel: state.panel ?? getLoadingPanel(state.title),
title, ...state,
key,
name,
panel: new VizPanel({
title,
menu: new VizPanelMenu({
$behaviors: [panelMenuBehavior],
}),
}),
}); });
this.addActivationHandler(this._onActivate); this.addActivationHandler(this._onActivate);
@ -41,29 +36,54 @@ export class LibraryVizPanel extends SceneObjectBase<LibraryVizPanelState> {
}; };
private async loadLibraryPanelFromPanelModel() { private async loadLibraryPanelFromPanelModel() {
let vizPanel = this.state.panel; let vizPanel = this.state.panel!;
try { try {
const libPanel = await getLibraryPanel(this.state.uid, true); const libPanel = await getLibraryPanel(this.state.uid, true);
if (this.state._loadedVersion === libPanel.version) {
return;
}
const libPanelModel = new PanelModel(libPanel.model); const libPanelModel = new PanelModel(libPanel.model);
vizPanel = vizPanel.clone({
const panel = new VizPanel({
title: this.state.title,
options: libPanelModel.options ?? {}, options: libPanelModel.options ?? {},
fieldConfig: libPanelModel.fieldConfig, fieldConfig: libPanelModel.fieldConfig,
pluginId: libPanelModel.type,
pluginVersion: libPanelModel.pluginVersion, pluginVersion: libPanelModel.pluginVersion,
displayMode: libPanelModel.transparent ? 'transparent' : undefined, displayMode: libPanelModel.transparent ? 'transparent' : undefined,
description: libPanelModel.description, description: libPanelModel.description,
pluginId: libPanel.type,
$data: createPanelDataProvider(libPanelModel), $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) { } catch (err) {
vizPanel.setState({ vizPanel.setState({
_pluginLoadError: 'Unable to load library panel: ' + this.state.uid, _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<LibraryVizPanel>) { function LibraryPanelRenderer({ model }: SceneComponentProps<LibraryVizPanel>) {
const { panel } = model.useState(); const { panel } = model.useState();

View File

@ -22,6 +22,7 @@ import {
defaultDashboard, defaultDashboard,
defaultTimePickerConfig, defaultTimePickerConfig,
FieldConfigSource, FieldConfigSource,
GridPos,
Panel, Panel,
RowPanel, RowPanel,
TimePickerConfig, TimePickerConfig,
@ -200,11 +201,22 @@ export function gridItemToPanel(gridItem: SceneGridItemLike, isSnapshot = false)
throw new Error('Unsupported grid item type'); 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 = { const panel: Panel = {
id: getPanelIdForVizPanel(vizPanel), id: getPanelIdForVizPanel(vizPanel),
type: vizPanel.state.pluginId, type: vizPanel.state.pluginId,
title: vizPanel.state.title, title: vizPanel.state.title,
gridPos: { x, y, w, h }, gridPos,
options: vizPanel.state.options, options: vizPanel.state.options,
fieldConfig: (vizPanel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] }, fieldConfig: (vizPanel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] },
transformations: [], transformations: [],
@ -241,7 +253,6 @@ export function gridItemToPanel(gridItem: SceneGridItemLike, isSnapshot = false)
if (!panel.transparent) { if (!panel.transparent) {
delete panel.transparent; delete panel.transparent;
} }
return panel; return panel;
} }