DashboardScenes: Use behavior for library panels (#90886)

* wip - refactor using libPanelBehavior

* wip

* wip

* wip

* migrate test

* wip

* nearly all tests done, except one commented which breaks

* migrate last test

* revert defaults.ini

* fix tests

* tests + fixes

* fix

* fix bug -- adding lib panel through CTA in empty dashboard didn't enter edit mode

* show unsaved lib panel changes modal

* fix height problems

* fix

* LibPanelBehavior: Fixes view panel and edit panel full page reload issues (alt fix)  (#92850)

* LibPanelsBehavior: Fixes view panel

* Sort of working except panel edit

* Got panel edit working

* LibraryPanelsBehavior: Simpler fix

* Cleanup

* Remove more stuff

* Update tests

* fix tests

* Work around timing issues

* remove log

* fix repeated panels not showing due to gridItem height not being set + translations

* remove log that affects tests

---------

Co-authored-by: Victor Marin <victor.marin@grafana.com>

* fix translations

* Fix save issue

---------

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
Victor Marin 2024-09-05 18:08:25 +03:00 committed by GitHub
parent d3ceaf41c2
commit 4a4e3a4063
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 961 additions and 1042 deletions

View File

@ -16,9 +16,8 @@ import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
import { DashboardGridItem } from '../../scene/DashboardGridItem';
import { DashboardScene } from '../../scene/DashboardScene';
import { LibraryVizPanel } from '../../scene/LibraryVizPanel';
import { gridItemToPanel, vizPanelToPanel } from '../../serialization/transformSceneToSaveModel';
import { getQueryRunnerFor } from '../../utils/utils';
import { getQueryRunnerFor, isLibraryPanel } from '../../utils/utils';
import { Randomize, randomizeData } from './randomizer';
@ -64,11 +63,10 @@ export function getGithubMarkdown(panel: VizPanel, snapshot: string): string {
export async function getDebugDashboard(panel: VizPanel, rand: Randomize, timeRange: TimeRange) {
let saveModel: ReturnType<typeof gridItemToPanel> = { type: '' };
const isLibraryPanel = panel.parent instanceof LibraryVizPanel;
const gridItem = (isLibraryPanel ? panel.parent.parent : panel.parent) as DashboardGridItem;
const gridItem = panel.parent as DashboardGridItem;
const scene = panel.getRoot() as DashboardScene;
if (isLibraryPanel) {
if (isLibraryPanel(panel)) {
saveModel = {
...gridItemToPanel(gridItem),
...vizPanelToPanel(panel),
@ -78,7 +76,7 @@ export async function getDebugDashboard(panel: VizPanel, rand: Randomize, timeRa
// we want the debug dashboard to include the panel with any changes that were made while
// in panel edit mode.
const sourcePanel = scene.state.editPanel.state.vizManager.state.sourcePanel.resolve();
const dashGridItem = sourcePanel.parent instanceof LibraryVizPanel ? sourcePanel.parent.parent : sourcePanel.parent;
const dashGridItem = sourcePanel.parent;
if (dashGridItem instanceof DashboardGridItem) {
saveModel = {
...gridItemToPanel(dashGridItem),

View File

@ -5,6 +5,8 @@ import {
getDefaultTimeRange,
LoadingState,
PanelData,
PanelPlugin,
PluginType,
standardTransformersRegistry,
toDataFrame,
} from '@grafana/data';
@ -16,7 +18,7 @@ import { getStandardTransformers } from 'app/features/transformers/standardTrans
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene } from '../scene/DashboardScene';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
import { activateFullSceneTree } from '../utils/test-utils';
@ -25,10 +27,33 @@ import { findVizPanelByKey } from '../utils/utils';
import { InspectJsonTab } from './InspectJsonTab';
standardTransformersRegistry.setInit(getStandardTransformers);
const panelPlugin: PanelPlugin = new PanelPlugin(() => null);
panelPlugin.meta = {
id: 'table',
name: 'Table',
sort: 1,
type: PluginType.panel,
info: {
author: {
name: 'name',
},
description: '',
links: [],
logos: {
large: '',
small: '',
},
screenshots: [],
updated: '',
version: '1.0.',
},
module: '',
baseUrl: '',
};
setPluginImportUtils({
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
getPanelPluginFromCache: (id: string) => undefined,
getPanelPluginFromCache: (id: string) => panelPlugin,
});
jest.mock('@grafana/runtime', () => ({
@ -204,7 +229,29 @@ async function buildTestScene() {
}
async function buildTestSceneWithLibraryPanel() {
const panel = vizPanelToPanel(buildTestPanel());
const libraryPanel = new VizPanel({
title: 'Panel A',
pluginId: 'table',
key: 'panel-12',
$behaviors: [new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: '111' })],
titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })],
$data: new SceneDataTransformer({
transformations: [
{
id: 'reduce',
options: {
reducers: ['last'],
},
},
],
$data: new SceneQueryRunner({
datasource: { uid: 'abcdef' },
queries: [{ refId: 'A' }],
}),
}),
});
const panel = vizPanelToPanel(libraryPanel.clone({ $behaviors: undefined }));
const libraryPanelState = {
name: 'LibraryPanel A',
@ -212,11 +259,20 @@ async function buildTestSceneWithLibraryPanel() {
uid: '111',
panelKey: 'panel-22',
model: panel,
type: 'table',
version: 1,
};
jest.spyOn(libpanels, 'getLibraryPanel').mockResolvedValue({ ...libraryPanelState, ...panel });
const libraryPanel = new LibraryVizPanel(libraryPanelState);
const gridItem = new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: libraryPanel,
});
const scene = new DashboardScene({
title: 'hello',
@ -225,16 +281,7 @@ async function buildTestSceneWithLibraryPanel() {
canEdit: true,
},
body: new SceneGridLayout({
children: [
new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: libraryPanel,
}),
],
children: [gridItem],
}),
});
@ -243,7 +290,7 @@ async function buildTestSceneWithLibraryPanel() {
await new Promise((r) => setTimeout(r, 1));
const tab = new InspectJsonTab({
panelRef: libraryPanel.state.panel!.getRef(),
panelRef: libraryPanel.getRef(),
onClose: jest.fn(),
});

View File

@ -27,10 +27,15 @@ import { reportPanelInspectInteraction } from 'app/features/search/page/reportin
import { VizPanelManager } from '../panel-edit/VizPanelManager';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { buildGridItemForPanel } from '../serialization/transformSaveModelToScene';
import { gridItemToPanel, vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
import {
getDashboardSceneFor,
getLibraryPanelBehavior,
getPanelIdForVizPanel,
getQueryRunnerFor,
isLibraryPanel,
} from '../utils/utils';
export type ShowContent = 'panel-json' | 'panel-data' | 'data-frames';
@ -142,7 +147,7 @@ export class InspectJsonTab extends SceneObjectBase<InspectJsonTabState> {
const panel = this.state.panelRef.resolve();
// Library panels are not editable from the inspect
if (panel.parent instanceof LibraryVizPanel) {
if (isLibraryPanel(panel)) {
return false;
}
@ -206,11 +211,11 @@ function getJsonText(show: ShowContent, panel: VizPanel): string {
case 'panel-json': {
reportPanelInspectInteraction(InspectTab.JSON, 'panelData');
const isInspectingLibraryPanel = panel.parent instanceof LibraryVizPanel;
const gridItem = isInspectingLibraryPanel ? panel.parent.parent : panel.parent;
const isInspectingLibraryPanel = isLibraryPanel(panel);
const gridItem = panel.parent;
if (isInspectingLibraryPanel) {
objToStringify = libraryPanelChildToLegacyRepresentation(panel);
objToStringify = libraryPanelToLegacyRepresentation(panel);
break;
}
@ -256,19 +261,20 @@ function getJsonText(show: ShowContent, panel: VizPanel): string {
/**
*
* @param panel Must be child of a LibraryVizPanel that is in turn the child of a DashboardGridItem
* @param panel Must hold a LibraryPanel behavior
* @returns object representation of the legacy library panel structure.
*/
function libraryPanelChildToLegacyRepresentation(panel: VizPanel<{}, {}>) {
if (!(panel.parent instanceof LibraryVizPanel)) {
throw 'Panel not child of LibraryVizPanel';
function libraryPanelToLegacyRepresentation(panel: VizPanel<{}, {}>) {
if (!isLibraryPanel(panel)) {
throw 'Panel not a library panel';
}
if (!(panel.parent.parent instanceof DashboardGridItem)) {
const gridItem = panel.parent;
if (!(gridItem instanceof DashboardGridItem)) {
throw 'LibraryPanel not child of DashboardGridItem';
}
const gridItem = panel.parent.parent;
const gridPos = {
x: gridItem.state.x || 0,
y: gridItem.state.y || 0,
@ -276,19 +282,26 @@ function libraryPanelChildToLegacyRepresentation(panel: VizPanel<{}, {}>) {
w: gridItem.state.width || 0,
};
const libraryPanelObj = vizPanelToLibraryPanel(panel);
const panelObj = vizPanelToPanel(panel, gridPos, false, gridItem);
const panelObj = vizPanelToPanel(panel.clone({ $behaviors: undefined }), gridPos, false, gridItem);
return { libraryPanel: { ...libraryPanelObj }, ...panelObj };
}
function vizPanelToLibraryPanel(panel: VizPanel): LibraryPanel {
if (!(panel.parent instanceof LibraryVizPanel)) {
throw new Error('Panel not a child of LibraryVizPanel');
if (!isLibraryPanel(panel)) {
throw new Error('Panel not a Library panel');
}
if (!panel.parent.state._loadedPanel) {
const libraryPanel = getLibraryPanelBehavior(panel);
if (!libraryPanel) {
throw new Error('Library panel behavior not found');
}
if (!libraryPanel.state._loadedPanel) {
throw new Error('Library panel not loaded');
}
return panel.parent.state._loadedPanel;
return libraryPanel.state._loadedPanel;
}
function hasGridPosChanged(a: SceneGridItemStateLike, b: SceneGridItemStateLike) {

View File

@ -3,10 +3,10 @@ import { css } from '@emotion/css';
import { GrafanaTheme2, dateTimeFormat } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
interface Props {
libraryPanel: LibraryVizPanel;
libraryPanel: LibraryPanelBehavior;
}
export const LibraryVizPanelInfo = ({ libraryPanel }: Props) => {

View File

@ -4,7 +4,7 @@ import * as libAPI from 'app/features/library-panels/state/api';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene } from '../scene/DashboardScene';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
import { activateFullSceneTree } from '../utils/test-utils';
@ -135,20 +135,19 @@ describe('PanelEditor', () => {
version: 1,
};
const libraryPanel = new LibraryVizPanel({
const libPanelBehavior = new LibraryPanelBehavior({
isLoaded: true,
title: libraryPanelModel.title,
uid: libraryPanelModel.uid,
name: libraryPanelModel.name,
panelKey: panel.state.key!,
panel: panel,
_loadedPanel: libraryPanelModel,
});
const gridItem = new DashboardGridItem({ body: libraryPanel });
const apiCall = jest
.spyOn(libAPI, 'updateLibraryVizPanel')
.mockResolvedValue({ type: 'panel', ...libAPI.libraryVizPanelToSaveModel(libraryPanel), version: 2 });
panel.setState({
$behaviors: [libPanelBehavior],
});
const gridItem = new DashboardGridItem({ body: panel });
const editScene = buildPanelEditScene(panel);
const scene = new DashboardScene({
@ -162,17 +161,23 @@ describe('PanelEditor', () => {
activateFullSceneTree(scene);
editScene.state.vizManager.state.panel.setState({ title: 'changed title' });
(editScene.state.vizManager.state.sourcePanel.resolve().parent as LibraryVizPanel).setState({
(editScene.state.vizManager.state.panel.state.$behaviors![0] as LibraryPanelBehavior).setState({
name: 'changed name',
});
jest.spyOn(libAPI, 'saveLibPanel').mockImplementation(async (panel) => {
const updatedPanel = { ...libAPI.libraryVizPanelToSaveModel(panel), version: 2 };
libPanelBehavior.setPanelFromLibPanel(updatedPanel);
});
editScene.state.vizManager.commitChanges();
const calledWith = apiCall.mock.calls[0][0].state;
expect(calledWith.panel?.state.title).toBe('changed title');
expect(calledWith.name).toBe('changed name');
await new Promise(process.nextTick); // Wait for mock api to return and update the library panel
expect((gridItem.state.body as LibraryVizPanel).state._loadedPanel?.version).toBe(2);
expect(libPanelBehavior.state._loadedPanel?.version).toBe(2);
expect(libPanelBehavior.state.name).toBe('changed name');
expect(libPanelBehavior.state.title).toBe('changed title');
expect((gridItem.state.body as VizPanel).state.title).toBe('changed title');
});
});

View File

@ -5,7 +5,6 @@ import { config, locationService } from '@grafana/runtime';
import { SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { getDashboardSceneFor, getPanelIdForVizPanel } from '../utils/utils';
import { PanelDataPane } from './PanelDataPane/PanelDataPane';
@ -93,6 +92,7 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
}
public onDiscard = () => {
this.state.vizManager.setState({ isDirty: false });
this._discardChanges = true;
locationService.partial({ editPanel: null });
};
@ -106,15 +106,7 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
const panelManager = this.state.vizManager;
const sourcePanel = panelManager.state.sourcePanel.resolve();
const sourcePanelParent = sourcePanel!.parent;
const isLibraryPanel = sourcePanelParent instanceof LibraryVizPanel;
const gridItem = isLibraryPanel ? sourcePanelParent.parent : sourcePanelParent;
if (isLibraryPanel) {
// Library panels handled separately
return;
}
const gridItem = sourcePanel!.parent;
if (!(gridItem instanceof DashboardGridItem)) {
console.error('Unsupported scene object type');
@ -155,6 +147,7 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
public onConfirmSaveLibraryPanel = () => {
this.state.vizManager.commitChanges();
this.state.vizManager.setState({ isDirty: false });
locationService.partial({ editPanel: null });
};

View File

@ -8,7 +8,7 @@ import { Button, ToolbarButton, useStyles2 } from '@grafana/ui';
import { NavToolbarActions } from '../scene/NavToolbarActions';
import { UnlinkModal } from '../scene/UnlinkModal';
import { getDashboardSceneFor, getLibraryPanel } from '../utils/utils';
import { getDashboardSceneFor, getLibraryPanelBehavior } from '../utils/utils';
import { PanelEditor } from './PanelEditor';
import { SaveLibraryVizPanelModal } from './SaveLibraryVizPanelModal';
@ -68,7 +68,7 @@ function VizAndDataPane({ model }: SceneComponentProps<PanelEditor>) {
const dashboard = getDashboardSceneFor(model);
const { vizManager, dataPane, showLibraryPanelSaveModal, showLibraryPanelUnlinkModal } = model.useState();
const { sourcePanel } = vizManager.useState();
const libraryPanel = getLibraryPanel(sourcePanel.resolve());
const libraryPanel = getLibraryPanelBehavior(sourcePanel.resolve());
const { controls } = dashboard.useState();
const styles = useStyles2(getStyles);

View File

@ -12,7 +12,7 @@ import { overrideRuleTooltipDescription } from 'app/features/dashboard/component
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene } from '../scene/DashboardScene';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
import { activateFullSceneTree } from '../utils/test-utils';
import * as utils from '../utils/utils';
@ -171,17 +171,19 @@ describe('PanelOptions', () => {
version: 1,
};
const libraryPanel = new LibraryVizPanel({
const libraryPanel = new LibraryPanelBehavior({
isLoaded: true,
title: libraryPanelModel.title,
uid: libraryPanelModel.uid,
name: libraryPanelModel.name,
panelKey: panel.state.key!,
panel: panel,
_loadedPanel: libraryPanelModel,
});
new DashboardGridItem({ body: libraryPanel });
panel.setState({
$behaviors: [libraryPanel],
});
new DashboardGridItem({ body: panel });
const { renderResult, vizManager } = setup({ panel: panel });
@ -191,7 +193,7 @@ describe('PanelOptions', () => {
fireEvent.blur(input, { target: { value: 'new library panel name' } });
});
expect((vizManager.state.sourcePanel.resolve().parent as LibraryVizPanel).state.name).toBe(
expect((vizManager.state.panel.state.$behaviors![0] as LibraryPanelBehavior).state.name).toBe(
'new library panel name'
);
});

View File

@ -2,6 +2,7 @@ import { useMemo } from 'react';
import * as React from 'react';
import { PanelData } from '@grafana/data';
import { VizPanel } from '@grafana/scenes';
import { OptionFilter, renderSearchHits } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions';
import { getFieldOverrideCategories } from 'app/features/dashboard/components/PanelEditor/getFieldOverrideElements';
import {
@ -9,7 +10,8 @@ import {
getVisualizationOptions2,
} from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { getLibraryPanelBehavior, isLibraryPanel } from '../utils/utils';
import { VizPanelManager } from './VizPanelManager';
import { getPanelFrameCategory2 } from './getPanelFrameOptions';
@ -22,8 +24,7 @@ interface Props {
}
export const PanelOptions = React.memo<Props>(({ vizManager, searchQuery, listMode, data }) => {
const { panel, sourcePanel, repeat } = vizManager.useState();
const parent = sourcePanel.resolve().parent;
const { panel, repeat } = vizManager.useState();
const { options, fieldConfig, _pluginInstanceState } = panel.useState();
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -49,11 +50,17 @@ export const PanelOptions = React.memo<Props>(({ vizManager, searchQuery, listMo
}, [panel, options, fieldConfig, _pluginInstanceState]);
const libraryPanelOptions = useMemo(() => {
if (parent instanceof LibraryVizPanel) {
return getLibraryVizPanelOptionsCategory(parent);
if (panel instanceof VizPanel && isLibraryPanel(panel)) {
const behavior = getLibraryPanelBehavior(panel);
if (!(behavior instanceof LibraryPanelBehavior)) {
return;
}
return getLibraryVizPanelOptionsCategory(behavior);
}
return;
}, [parent]);
}, [panel]);
const justOverrides = useMemo(
() =>

View File

@ -5,10 +5,10 @@ import { Button, Icon, Input, Modal, useStyles2 } from '@grafana/ui';
import { getConnectedDashboards } from 'app/features/library-panels/state/api';
import { getModalStyles } from 'app/features/library-panels/styles';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
interface Props {
libraryPanel: LibraryVizPanel;
libraryPanel: LibraryPanelBehavior;
isUnsavedPrompt?: boolean;
onConfirm: () => void;
onDismiss: () => void;

View File

@ -20,7 +20,7 @@ import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange';
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
@ -286,28 +286,28 @@ describe('VizPanelManager', () => {
version: 1,
};
const libraryPanel = new LibraryVizPanel({
const libPanelBehavior = new LibraryPanelBehavior({
isLoaded: true,
title: libraryPanelModel.title,
uid: libraryPanelModel.uid,
name: libraryPanelModel.name,
panelKey: panel.state.key!,
panel: panel,
_loadedPanel: libraryPanelModel,
});
new DashboardGridItem({ body: libraryPanel });
panel.setState({
$behaviors: [libPanelBehavior],
});
new DashboardGridItem({ body: panel });
const panelManager = VizPanelManager.createFor(panel);
const apiCall = jest
.spyOn(libAPI, 'updateLibraryVizPanel')
.mockResolvedValue({ type: 'panel', ...libAPI.libraryVizPanelToSaveModel(libraryPanel) });
const apiCall = jest.spyOn(libAPI, 'saveLibPanel');
panelManager.state.panel.setState({ title: 'new title' });
panelManager.commitChanges();
expect(apiCall.mock.calls[0][0].state.panel?.state.title).toBe('new title');
expect(apiCall.mock.calls[0][0].state.title).toBe('new title');
});
it('unlinks library panel', () => {
@ -325,23 +325,25 @@ describe('VizPanelManager', () => {
version: 1,
};
const libraryPanel = new LibraryVizPanel({
const libPanelBehavior = new LibraryPanelBehavior({
isLoaded: true,
title: libraryPanelModel.title,
uid: libraryPanelModel.uid,
name: libraryPanelModel.name,
panelKey: panel.state.key!,
panel: panel,
_loadedPanel: libraryPanelModel,
});
const gridItem = new DashboardGridItem({ body: libraryPanel });
panel.setState({
$behaviors: [libPanelBehavior],
});
new DashboardGridItem({ body: panel });
const panelManager = VizPanelManager.createFor(panel);
panelManager.unlinkLibraryPanel();
const sourcePanel = panelManager.state.sourcePanel.resolve();
expect(sourcePanel.parent?.state.key).toBe(gridItem.state.key);
expect(sourcePanel.state.$behaviors).toBe(undefined);
});
});

View File

@ -34,7 +34,7 @@ import { DataQuery, DataTransformerConfig, Panel } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui';
import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard';
import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils';
import { updateLibraryVizPanel } from 'app/features/library-panels/state/api';
import { saveLibPanel } from 'app/features/library-panels/state/api';
import { updateQueries } from 'app/features/query/state/updateQueries';
import { GrafanaQuery } from 'app/plugins/datasource/grafana/types';
import { QueryGroupOptions } from 'app/types';
@ -42,10 +42,15 @@ import { QueryGroupOptions } from 'app/types';
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
import { getPanelChanges } from '../saving/getDashboardChanges';
import { DashboardGridItem, RepeatDirection } from '../scene/DashboardGridItem';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange';
import { gridItemToPanel, vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
import { getDashboardSceneFor, getMultiVariableValues, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
import {
getDashboardSceneFor,
getMultiVariableValues,
getPanelIdForVizPanel,
getQueryRunnerFor,
isLibraryPanel,
} from '../utils/utils';
export interface VizPanelManagerState extends SceneObjectState {
panel: VizPanel;
@ -85,7 +90,7 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
public static createFor(sourcePanel: VizPanel) {
let repeatOptions: Pick<VizPanelManagerState, 'repeat' | 'repeatDirection' | 'maxPerRow'> = {};
const gridItem = sourcePanel.parent instanceof LibraryVizPanel ? sourcePanel.parent.parent : sourcePanel.parent;
const gridItem = sourcePanel.parent;
if (!(gridItem instanceof DashboardGridItem)) {
console.error('VizPanel is not a child of a dashboard grid item');
@ -143,8 +148,8 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
private _detectPanelModelChanges = debounce(() => {
const { hasChanges } = getPanelChanges(
vizPanelToPanel(this.state.sourcePanel.resolve()),
vizPanelToPanel(this.state.panel)
vizPanelToPanel(this.state.sourcePanel.resolve().clone({ $behaviors: undefined })),
vizPanelToPanel(this.state.panel.clone({ $behaviors: undefined }))
);
this.setState({ isDirty: hasChanges });
}, 250);
@ -263,10 +268,7 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
});
}
this.setState({
pluginId,
});
this.setState({ pluginId });
this.state.panel.changePluginType(pluginId, cachedOptions, newFieldConfig);
this.loadDataSource();
@ -405,21 +407,22 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
public unlinkLibraryPanel() {
const sourcePanel = this.state.sourcePanel.resolve();
if (!(sourcePanel.parent instanceof LibraryVizPanel)) {
throw new Error('VizPanel is not a child of a library panel');
if (!isLibraryPanel(sourcePanel)) {
throw new Error('VizPanel is not a library panel');
}
const gridItem = sourcePanel.parent.parent;
const gridItem = sourcePanel.parent;
if (!(gridItem instanceof DashboardGridItem)) {
throw new Error('Library panel not a child of a grid item');
}
const newSourcePanel = this.state.panel.clone({ $data: this.state.$data?.clone() });
const newSourcePanel = this.state.panel.clone({ $data: sourcePanel.state.$data?.clone(), $behaviors: undefined });
gridItem.setState({
body: newSourcePanel,
});
this.state.panel.setState({ $behaviors: undefined });
this.setState({ sourcePanel: newSourcePanel.getRef() });
}
@ -435,30 +438,17 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
maxPerRow: this.state.maxPerRow,
};
const vizPanel = this.state.panel.clone();
if (sourcePanel.parent instanceof DashboardGridItem) {
sourcePanel.parent.setState({
...repeatUpdate,
body: this.state.panel.clone(),
body: vizPanel,
});
}
if (sourcePanel.parent instanceof LibraryVizPanel) {
if (sourcePanel.parent.parent instanceof DashboardGridItem) {
const newLibPanel = sourcePanel.parent.clone({
panel: this.state.panel.clone(),
});
sourcePanel.parent.parent.setState({
body: newLibPanel,
...repeatUpdate,
});
updateLibraryVizPanel(newLibPanel!).then((p) => {
if (sourcePanel.parent instanceof LibraryVizPanel) {
newLibPanel.setPanelFromLibPanel(p);
}
});
}
if (isLibraryPanel(vizPanel)) {
saveLibPanel(vizPanel);
}
}
@ -467,9 +457,7 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
*/
public getPanelSaveModel(): Panel | object {
const sourcePanel = this.state.sourcePanel.resolve();
const isLibraryPanel = sourcePanel.parent instanceof LibraryVizPanel;
const gridItem = isLibraryPanel ? sourcePanel.parent.parent : sourcePanel.parent;
const gridItem = sourcePanel.parent;
if (!(gridItem instanceof DashboardGridItem)) {
return { error: 'Unsupported panel parent' };

View File

@ -8,7 +8,9 @@ import { Dashboard } from '@grafana/schema/dist/esm/index.gen';
import { ModalsContext, Modal, Button, useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { SaveLibraryVizPanelModal } from '../panel-edit/SaveLibraryVizPanelModal';
import { DashboardScene } from '../scene/DashboardScene';
import { getLibraryPanelBehavior, isLibraryPanel } from '../utils/utils';
interface DashboardPromptProps {
dashboard: DashboardScene;
@ -38,29 +40,39 @@ export const DashboardPrompt = memo(({ dashboard }: DashboardPromptProps) => {
}, [dashboard]);
const onHistoryBlock = (location: H.Location) => {
// const panelInEdit = dashboard.state.editPanel;
// const search = new URLSearchParams(location.search);
const panelInEdit = dashboard.state.editPanel;
const vizPanelManager = panelInEdit?.state.vizManager;
const vizPanel = vizPanelManager?.state.panel;
const search = new URLSearchParams(location.search);
// TODO: Are we leaving panel edit & library panel?
// Are we leaving panel edit & library panel?
if (
panelInEdit &&
vizPanel &&
isLibraryPanel(vizPanel) &&
vizPanelManager.state.isDirty &&
!search.has('editPanel')
) {
const libPanelBehavior = getLibraryPanelBehavior(vizPanel);
// if (panelInEdit && panelInEdit.libraryPanel && panelInEdit.hasChanged && !search.has('editPanel')) {
// showModal(SaveLibraryPanelModal, {
// isUnsavedPrompt: true,
// panel: dashboard.panelInEdit as PanelModelWithLibraryPanel,
// folderUid: dashboard.meta.folderUid ?? '',
// onConfirm: () => {
// hideModal();
// moveToBlockedLocationAfterReactStateUpdate(location);
// },
// onDiscard: () => {
// dispatch(discardPanelChanges());
// moveToBlockedLocationAfterReactStateUpdate(location);
// hideModal();
// },
// onDismiss: hideModal,
// });
// return false;
// }
showModal(SaveLibraryVizPanelModal, {
dashboard,
isUnsavedPrompt: true,
libraryPanel: libPanelBehavior!,
onConfirm: () => {
panelInEdit.onConfirmSaveLibraryPanel();
hideModal();
moveToBlockedLocationAfterReactStateUpdate(location);
},
onDiscard: () => {
panelInEdit.onDiscard();
hideModal();
moveToBlockedLocationAfterReactStateUpdate(location);
},
onDismiss: hideModal,
});
return false;
}
// Are we still on the same dashboard?
if (originalPath === location.pathname) {

View File

@ -19,7 +19,7 @@ import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsData
import { DashboardControls } from '../scene/DashboardControls';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene, PERSISTED_PROPS } from '../scene/DashboardScene';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { VizPanelLinks } from '../scene/PanelLinks';
import { PanelTimeRange } from '../scene/PanelTimeRange';
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
@ -77,11 +77,6 @@ export class DashboardSceneChangeTracker {
if (payload.changedObject instanceof VizPanelLinks) {
return true;
}
if (payload.changedObject instanceof LibraryVizPanel) {
if (Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'name')) {
return true;
}
}
if (payload.changedObject instanceof SceneRefreshPicker) {
if (
Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'intervals') ||
@ -90,6 +85,11 @@ export class DashboardSceneChangeTracker {
return true;
}
}
if (payload.changedObject instanceof LibraryPanelBehavior) {
if (Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'name')) {
return true;
}
}
if (payload.changedObject instanceof behaviors.CursorSync) {
return true;
}

View File

@ -1,4 +1,4 @@
import { SceneGridLayout, SceneGridRow, SceneTimeRange } from '@grafana/scenes';
import { SceneGridLayout, SceneGridRow, SceneTimeRange, VizPanel } from '@grafana/scenes';
import { LibraryPanel } from '@grafana/schema/dist/esm/index.gen';
import { activateFullSceneTree } from '../utils/test-utils';
@ -6,7 +6,17 @@ import { activateFullSceneTree } from '../utils/test-utils';
import { AddLibraryPanelDrawer } from './AddLibraryPanelDrawer';
import { DashboardGridItem } from './DashboardGridItem';
import { DashboardScene } from './DashboardScene';
import { LibraryVizPanel } from './LibraryVizPanel';
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getDataSourceSrv: () => {
return {
get: jest.fn().mockResolvedValue({}),
getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds1' }),
};
},
}));
describe('AddLibraryPanelWidget', () => {
let dashboard: DashboardScene;
@ -35,8 +45,55 @@ describe('AddLibraryPanelWidget', () => {
const gridItem = layout.state.children[0] as DashboardGridItem;
expect(layout.state.children.length).toBe(1);
expect(gridItem.state.body!).toBeInstanceOf(LibraryVizPanel);
expect((gridItem.state.body! as LibraryVizPanel).state.panelKey).toBe('panel-1');
expect(gridItem.state.body!).toBeInstanceOf(VizPanel);
expect(gridItem.state.body.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior);
expect(gridItem.state.body.state.key).toBe('panel-1');
});
it('should add library panel from menu and enter edit mode in a dashboard that is not already in edit mode', async () => {
const drawer = new AddLibraryPanelDrawer({});
const dashboard = new DashboardScene({
$timeRange: new SceneTimeRange({}),
title: 'hello',
uid: 'dash-1',
version: 4,
meta: {
canEdit: true,
},
body: new SceneGridLayout({
children: [],
}),
overlay: drawer,
});
activateFullSceneTree(dashboard);
await new Promise((r) => setTimeout(r, 1));
const panelInfo: LibraryPanel = {
uid: 'uid',
model: {
type: 'timeseries',
},
name: 'name',
version: 1,
type: 'timeseries',
};
// if we are in a saved dashboard with no panels, adding a lib panel through
// the CTA should enter edit mode
expect(dashboard.state.isEditing).toBe(undefined);
drawer.onAddLibraryPanel(panelInfo);
const layout = dashboard.state.body as SceneGridLayout;
const gridItem = layout.state.children[0] as DashboardGridItem;
expect(layout.state.children.length).toBe(1);
expect(gridItem.state.body!).toBeInstanceOf(VizPanel);
expect(gridItem.state.body.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior);
expect(gridItem.state.body.state.key).toBe('panel-1');
expect(dashboard.state.isEditing).toBe(true);
});
it('should throw error if adding lib panel in a layout that is not SceneGridLayout', () => {
@ -48,11 +105,11 @@ describe('AddLibraryPanelWidget', () => {
});
it('should replace grid item when grid item state is passed', async () => {
const libPanel = new LibraryVizPanel({
const libPanel = new VizPanel({
title: 'Panel Title',
uid: 'uid',
name: 'name',
panelKey: 'panel-1',
pluginId: 'table',
key: 'panel-1',
$behaviors: [new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: 'uid' })],
});
let gridItem = new DashboardGridItem({
@ -88,20 +145,22 @@ describe('AddLibraryPanelWidget', () => {
const layout = dashboard.state.body as SceneGridLayout;
gridItem = layout.state.children[0] as DashboardGridItem;
const behavior = gridItem.state.body!.state.$behaviors![0] as LibraryPanelBehavior;
expect(layout.state.children.length).toBe(1);
expect(gridItem.state.body!).toBeInstanceOf(LibraryVizPanel);
expect(gridItem.state.body!).toBeInstanceOf(VizPanel);
expect(behavior).toBeInstanceOf(LibraryPanelBehavior);
expect(gridItem.state.key).toBe('grid-item-1');
expect((gridItem.state.body! as LibraryVizPanel).state.uid).toBe('new_uid');
expect((gridItem.state.body! as LibraryVizPanel).state.name).toBe('new_name');
expect(behavior.state.uid).toBe('new_uid');
expect(behavior.state.name).toBe('new_name');
});
it('should replace grid item in row when grid item state is passed', async () => {
const libPanel = new LibraryVizPanel({
const libPanel = new VizPanel({
title: 'Panel Title',
uid: 'uid',
name: 'name',
panelKey: 'panel-1',
pluginId: 'table',
key: 'panel-1',
$behaviors: [new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: 'uid' })],
});
let gridItem = new DashboardGridItem({
@ -142,13 +201,15 @@ describe('AddLibraryPanelWidget', () => {
const layout = dashboard.state.body as SceneGridLayout;
const gridRow = layout.state.children[0] as SceneGridRow;
gridItem = gridRow.state.children[0] as DashboardGridItem;
const behavior = gridItem.state.body!.state.$behaviors![0] as LibraryPanelBehavior;
expect(layout.state.children.length).toBe(1);
expect(gridRow.state.children.length).toBe(1);
expect(gridItem.state.body!).toBeInstanceOf(LibraryVizPanel);
expect(gridItem.state.body!).toBeInstanceOf(VizPanel);
expect(behavior).toBeInstanceOf(LibraryPanelBehavior);
expect(gridItem.state.key).toBe('grid-item-1');
expect((gridItem.state.body! as LibraryVizPanel).state.uid).toBe('new_uid');
expect((gridItem.state.body! as LibraryVizPanel).state.name).toBe('new_name');
expect(behavior.state.uid).toBe('new_uid');
expect(behavior.state.name).toBe('new_name');
});
});

View File

@ -4,6 +4,7 @@ import {
SceneObjectBase,
SceneObjectRef,
SceneObjectState,
VizPanel,
} from '@grafana/scenes';
import { LibraryPanel } from '@grafana/schema';
import { Drawer } from '@grafana/ui';
@ -14,13 +15,13 @@ import {
} from 'app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { NEW_PANEL_HEIGHT, NEW_PANEL_WIDTH, getDashboardSceneFor, getVizPanelKeyForPanelId } from '../utils/utils';
import { NEW_PANEL_HEIGHT, NEW_PANEL_WIDTH, getDashboardSceneFor, getDefaultVizPanel } from '../utils/utils';
import { DashboardGridItem } from './DashboardGridItem';
import { LibraryVizPanel } from './LibraryVizPanel';
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
export interface AddLibraryPanelDrawerState extends SceneObjectState {
panelToReplaceRef?: SceneObjectRef<LibraryVizPanel>;
panelToReplaceRef?: SceneObjectRef<VizPanel>;
}
export class AddLibraryPanelDrawer extends SceneObjectBase<AddLibraryPanelDrawerState> {
@ -38,11 +39,9 @@ export class AddLibraryPanelDrawer extends SceneObjectBase<AddLibraryPanelDrawer
const panelId = dashboardSceneGraph.getNextPanelId(dashboard);
const body = new LibraryVizPanel({
title: 'Panel Title',
uid: panelInfo.uid,
name: panelInfo.name,
panelKey: getVizPanelKeyForPanelId(panelId),
const body = getDefaultVizPanel(dashboard);
body.setState({
$behaviors: [new LibraryPanelBehavior({ uid: panelInfo.uid, name: panelInfo.name })],
});
const panelToReplace = this.state.panelToReplaceRef?.resolve();
@ -66,6 +65,10 @@ export class AddLibraryPanelDrawer extends SceneObjectBase<AddLibraryPanelDrawer
});
layout.setState({ children: [newGridItem, ...layout.state.children] });
if (!dashboard.state.isEditing) {
dashboard.onEnterEditMode();
}
}
this.onClose();

View File

@ -21,7 +21,7 @@ import { activateFullSceneTree } from '../utils/test-utils';
import { DashboardDatasourceBehaviour } from './DashboardDatasourceBehaviour';
import { DashboardGridItem } from './DashboardGridItem';
import { DashboardScene } from './DashboardScene';
import { LibraryVizPanel } from './LibraryVizPanel';
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
const grafanaDs = {
id: 1,
@ -491,16 +491,23 @@ describe('DashboardDatasourceBehaviour', () => {
});
describe('Library panels', () => {
it('should wait for library panel to be loaded', async () => {
const sourcePanel = new LibraryVizPanel({
name: 'My Library Panel',
it('should re-run queries when library panel re-runs query', async () => {
const libPanelBehavior = new LibraryPanelBehavior({
isLoaded: false,
title: 'Panel title',
uid: 'fdcvggvfy2qdca',
panelKey: 'lib-panel',
panel: new VizPanel({
key: 'panel-1',
title: 'Panel A',
pluginId: 'table',
name: 'My Library Panel',
_loadedPanel: undefined,
});
const sourcePanel = new VizPanel({
key: 'panel-1',
title: 'Panel A',
pluginId: 'table',
$behaviors: [libPanelBehavior],
$data: new SceneQueryRunner({
datasource: { uid: 'grafana' },
queries: [{ refId: 'A', queryType: 'randomWalk' }],
}),
});
@ -544,29 +551,22 @@ describe('DashboardDatasourceBehaviour', () => {
}),
});
activateFullSceneTree(scene);
const sceneDeactivate = activateFullSceneTree(scene);
// spy on runQueries
const spy = jest.spyOn(dashboardDSPanel.state.$data as SceneQueryRunner, 'runQueries');
// deactivate scene to mimic going into panel edit
sceneDeactivate();
// run source panel queries and update request ID
(sourcePanel.state.$data as SceneQueryRunner).runQueries();
// // activate scene to mimic coming back from panel edit
activateFullSceneTree(scene);
await new Promise((r) => setTimeout(r, 1));
expect(spy).not.toHaveBeenCalled();
// Simulate library panel being loaded
sourcePanel.setState({
isLoaded: true,
panel: new VizPanel({
title: 'Panel A',
pluginId: 'table',
key: 'panel-1',
$data: new SceneQueryRunner({
datasource: { uid: 'grafana' },
queries: [{ refId: 'A', queryType: 'randomWalk' }],
}),
}),
});
expect(spy).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,18 +1,9 @@
import { Unsubscribable } from 'rxjs';
import {
CancelActivationHandler,
SceneObjectBase,
SceneObjectState,
SceneQueryRunner,
VizPanel,
} from '@grafana/scenes';
import { SceneObjectBase, SceneObjectState, SceneQueryRunner, VizPanel } from '@grafana/scenes';
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
import { findVizPanelByKey, getDashboardSceneFor, getQueryRunnerFor, getVizPanelKeyForPanelId } from '../utils/utils';
import { DashboardScene } from './DashboardScene';
import { LibraryVizPanel, LibraryVizPanelState } from './LibraryVizPanel';
interface DashboardDatasourceBehaviourState extends SceneObjectState {}
@ -27,8 +18,6 @@ export class DashboardDatasourceBehaviour extends SceneObjectBase<DashboardDatas
private _activationHandler() {
const queryRunner = this.parent;
let dashboard: DashboardScene;
let libraryPanelSub: Unsubscribable;
if (!(queryRunner instanceof SceneQueryRunner)) {
throw new Error('DashboardDatasourceBehaviour must be attached to a SceneQueryRunner');
}
@ -59,47 +48,16 @@ export class DashboardDatasourceBehaviour extends SceneObjectBase<DashboardDatas
const sourcePanelQueryRunner = getQueryRunnerFor(panel);
let parentLibPanelCleanUp: undefined | CancelActivationHandler;
if (!sourcePanelQueryRunner) {
if (!(panel.parent instanceof LibraryVizPanel)) {
throw new Error('Could not find SceneQueryRunner for panel');
} else {
if (!panel.parent.isActive) {
parentLibPanelCleanUp = panel.parent.activate();
}
// Library panels load and create internal viz panel asynchroniously. Here we are subscribing to
// library panel state, and run dashboard queries when the source panel query runner is ready.
libraryPanelSub = panel.parent.subscribeToState((n, p) => {
this.handleLibPanelStateUpdates(n, p, queryRunner);
});
}
} else {
if (this.prevRequestId && this.prevRequestId !== sourcePanelQueryRunner.state.data?.request?.requestId) {
queryRunner.runQueries();
}
throw new Error('Could not find SceneQueryRunner for panel');
}
if (this.prevRequestId && this.prevRequestId !== sourcePanelQueryRunner.state.data?.request?.requestId) {
queryRunner.runQueries();
}
return () => {
this.prevRequestId = sourcePanelQueryRunner?.state.data?.request?.requestId;
if (libraryPanelSub) {
libraryPanelSub.unsubscribe();
}
if (parentLibPanelCleanUp) {
parentLibPanelCleanUp();
}
};
}
private handleLibPanelStateUpdates(n: LibraryVizPanelState, p: LibraryVizPanelState, queryRunner: SceneQueryRunner) {
if (n.panel && n.panel !== p.panel) {
const libPanelQueryRunner = getQueryRunnerFor(n.panel);
if (!(libPanelQueryRunner instanceof SceneQueryRunner)) {
throw new Error('Could not find SceneQueryRunner for panel');
}
queryRunner.runQueries();
}
}
}

View File

@ -1,7 +1,6 @@
import { css } from '@emotion/css';
import { isEqual } from 'lodash';
import { useMemo } from 'react';
import { Unsubscribable } from 'rxjs';
import { config } from '@grafana/runtime';
import {
@ -26,13 +25,11 @@ import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
import { getMultiVariableValues, getQueryRunnerFor } from '../utils/utils';
import { AddLibraryPanelDrawer } from './AddLibraryPanelDrawer';
import { LibraryVizPanel } from './LibraryVizPanel';
import { repeatPanelMenuBehavior } from './PanelMenuBehavior';
import { DashboardRepeatsProcessedEvent } from './types';
export interface DashboardGridItemState extends SceneGridItemStateLike {
body: VizPanel | LibraryVizPanel | AddLibraryPanelDrawer;
body: VizPanel;
repeatedPanels?: VizPanel[];
variableName?: string;
itemHeight?: number;
@ -43,9 +40,8 @@ export interface DashboardGridItemState extends SceneGridItemStateLike {
export type RepeatDirection = 'v' | 'h';
export class DashboardGridItem extends SceneObjectBase<DashboardGridItemState> implements SceneGridItemLike {
private _libPanelSubscription: Unsubscribable | undefined;
private _prevRepeatValues?: VariableValueSingle[];
private _oldBody?: VizPanel | LibraryVizPanel | AddLibraryPanelDrawer;
private _oldBody?: VizPanel;
protected _variableDependency = new DashboardGridItemVariableDependencyHandler(this);
@ -65,45 +61,6 @@ export class DashboardGridItem extends SceneObjectBase<DashboardGridItemState> i
this._oldBody = this.state.body;
this.performRepeat();
}
// Subscriptions that handles body updates, i.e. VizPanel -> LibraryVizPanel, AddLibPanelWidget -> LibraryVizPanel
this._subs.add(
this.subscribeToState((newState, prevState) => {
if (newState.body !== prevState.body) {
if (newState.body instanceof LibraryVizPanel) {
this.setupLibraryPanelChangeSubscription(newState.body);
}
}
})
);
// Initial setup of the lbrary panel subscription. Lib panels are lazy laded, so only then we can subscribe to the repeat config changes
if (this.state.body instanceof LibraryVizPanel) {
this.setupLibraryPanelChangeSubscription(this.state.body);
}
return () => {
this._libPanelSubscription?.unsubscribe();
this._libPanelSubscription = undefined;
};
}
private setupLibraryPanelChangeSubscription(panel: LibraryVizPanel) {
if (this._libPanelSubscription) {
this._libPanelSubscription.unsubscribe();
this._libPanelSubscription = undefined;
}
this._libPanelSubscription = panel.subscribeToState((newState) => {
if (newState._loadedPanel?.model.repeat) {
this.setState({
variableName: newState._loadedPanel.model.repeat,
repeatDirection: newState._loadedPanel.model.repeatDirection,
maxPerRow: newState._loadedPanel.model.maxPerRow,
});
this.performRepeat();
}
});
}
/**
@ -132,10 +89,6 @@ export class DashboardGridItem extends SceneObjectBase<DashboardGridItemState> i
}
public performRepeat() {
if (this.state.body instanceof AddLibraryPanelDrawer) {
return;
}
if (!this.state.variableName || sceneGraph.hasVariableDependencyInLoadingState(this)) {
return;
}
@ -165,7 +118,7 @@ export class DashboardGridItem extends SceneObjectBase<DashboardGridItemState> i
}
this._prevRepeatValues = values;
const panelToRepeat = this.state.body instanceof LibraryVizPanel ? this.state.body.state.panel! : this.state.body;
const panelToRepeat = this.state.body;
const repeatedPanels: VizPanel[] = [];
// when variable has no options (due to error or similar) it will not render any panels at all
@ -262,14 +215,6 @@ export class DashboardGridItem extends SceneObjectBase<DashboardGridItemState> i
if (body instanceof VizPanel) {
return <body.Component model={body} key={body.state.key} />;
}
if (body instanceof LibraryVizPanel) {
return <body.Component model={body} key={body.state.key} />;
}
if (body instanceof AddLibraryPanelDrawer) {
return <body.Component model={body} key={body.state.key} />;
}
}
if (!repeatedPanels) {

View File

@ -20,11 +20,7 @@ import { VariablesChanged } from 'app/features/variables/types';
import { PanelEditor, buildPanelEditScene } from '../panel-edit/PanelEditor';
import { createWorker } from '../saving/createDetectChangesWorker';
import {
buildGridItemForLibPanel,
buildGridItemForPanel,
transformSaveModelToScene,
} from '../serialization/transformSaveModelToScene';
import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
import { DecoratedRevisionModel } from '../settings/VersionsEditView';
import { historySrv } from '../settings/version-history/HistorySrv';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
@ -34,7 +30,7 @@ import { findVizPanelByKey } from '../utils/utils';
import { DashboardControls } from './DashboardControls';
import { DashboardGridItem } from './DashboardGridItem';
import { DashboardScene, DashboardSceneState } from './DashboardScene';
import { LibraryVizPanel } from './LibraryVizPanel';
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
import { PanelTimeRange } from './PanelTimeRange';
import { RowActions } from './row-actions/RowActions';
@ -348,20 +344,21 @@ describe('DashboardScene', () => {
expect(restoredDashboardGridItem.state.variableName).toBe(prevValue);
});
it('A change to any LibraryVizPanel name should set isDirty true', () => {
const libraryVizPanel = sceneGraph.findObject(scene, (p) => p instanceof LibraryVizPanel) as LibraryVizPanel;
const prevValue = libraryVizPanel.state.name;
it('A change to any library panel name should set isDirty true', () => {
const libraryVizPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem).state
.body;
const behavior = libraryVizPanel.state.$behaviors![0] as LibraryPanelBehavior;
const prevValue = behavior.state.name;
libraryVizPanel.setState({ name: 'new name' });
behavior.setState({ name: 'new name' });
expect(scene.state.isDirty).toBe(true);
scene.exitEditMode({ skipConfirm: true });
const restoredLibraryVizPanel = sceneGraph.findObject(
scene,
(p) => p instanceof LibraryVizPanel
) as LibraryVizPanel;
expect(restoredLibraryVizPanel.state.name).toBe(prevValue);
const restoredLibraryVizPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem)
.state.body;
const restoredBehavior = restoredLibraryVizPanel.state.$behaviors![0] as LibraryPanelBehavior;
expect(restoredBehavior.state.name).toBe(prevValue);
});
it('A change to any PanelTimeRange state should set isDirty true', () => {
@ -625,19 +622,14 @@ describe('DashboardScene', () => {
});
it('Should fail to copy a library panel if it does not have a grid item parent', () => {
const libVizPanel = new LibraryVizPanel({
uid: 'uid',
name: 'libraryPanel',
panelKey: 'panel-4',
const libVizPanel = new VizPanel({
title: 'Library Panel',
panel: new VizPanel({
title: 'Library Panel',
key: 'panel-4',
pluginId: 'table',
}),
pluginId: 'table',
key: 'panel-4',
$behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })],
});
scene.copyPanel(libVizPanel.state.panel as VizPanel);
scene.copyPanel(libVizPanel);
expect(store.exists(LS_PANEL_COPY_KEY)).toBe(false);
});
@ -650,10 +642,11 @@ describe('DashboardScene', () => {
});
it('Should copy a library viz panel', () => {
const libVizPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem).state
.body as LibraryVizPanel;
const libVizPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem).state.body;
scene.copyPanel(libVizPanel.state.panel as VizPanel);
expect(libVizPanel.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior);
scene.copyPanel(libVizPanel);
expect(store.exists(LS_PANEL_COPY_KEY)).toBe(true);
});
@ -687,13 +680,13 @@ describe('DashboardScene', () => {
it('Should paste a library viz panel', () => {
store.set(LS_PANEL_COPY_KEY, JSON.stringify({ key: 'panel-7' }));
jest.spyOn(JSON, 'parse').mockReturnValue({ libraryPanel: { uid: 'uid', name: 'libraryPanel' } });
jest.mocked(buildGridItemForLibPanel).mockReturnValue(
jest.mocked(buildGridItemForPanel).mockReturnValue(
new DashboardGridItem({
body: new LibraryVizPanel({
body: new VizPanel({
title: 'Library Panel',
uid: 'uid',
name: 'libraryPanel',
panelKey: 'panel-4',
pluginId: 'table',
key: 'panel-4',
$behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })],
}),
})
);
@ -703,12 +696,11 @@ describe('DashboardScene', () => {
const body = scene.state.body as SceneGridLayout;
const gridItem = body.state.children[0] as DashboardGridItem;
const libVizPanel = gridItem.state.body as LibraryVizPanel;
const libVizPanel = gridItem.state.body;
expect(buildGridItemForLibPanel).toHaveBeenCalledTimes(1);
expect(buildGridItemForPanel).toHaveBeenCalledTimes(1);
expect(body.state.children.length).toBe(6);
expect(libVizPanel.state.panelKey).toBe('panel-7');
expect(libVizPanel.state.panel?.state.key).toBe('panel-7');
expect(libVizPanel.state.key).toBe('panel-7');
expect(gridItem.state.y).toBe(0);
expect(store.exists(LS_PANEL_COPY_KEY)).toBe(false);
});
@ -736,8 +728,7 @@ describe('DashboardScene', () => {
it('Should remove a library panel', () => {
const libraryPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem).state.body;
const vizPanel = (libraryPanel as LibraryVizPanel).state.panel;
scene.removePanel(vizPanel as VizPanel);
scene.removePanel(libraryPanel);
const body = scene.state.body as SceneGridLayout;
expect(body.state.children.length).toBe(4);
@ -748,9 +739,8 @@ describe('DashboardScene', () => {
((scene.state.body as SceneGridLayout).state.children[2] as SceneGridRow).state
.children[1] as DashboardGridItem
).state.body;
const vizPanel = (libraryPanel as LibraryVizPanel).state.panel;
scene.removePanel(vizPanel as VizPanel);
scene.removePanel(libraryPanel);
const body = scene.state.body as SceneGridLayout;
const gridRow = body.state.children[2] as SceneGridRow;
@ -784,17 +774,15 @@ describe('DashboardScene', () => {
it('Should duplicate a library panel', () => {
const libraryPanel = ((scene.state.body as SceneGridLayout).state.children[4] as DashboardGridItem).state.body;
const vizPanel = (libraryPanel as LibraryVizPanel).state.panel;
scene.duplicatePanel(vizPanel as VizPanel);
scene.duplicatePanel(libraryPanel);
const body = scene.state.body as SceneGridLayout;
const gridItem = body.state.children[5] as DashboardGridItem;
const libVizPanel = gridItem.state.body as LibraryVizPanel;
const libVizPanel = gridItem.state.body;
expect(body.state.children.length).toBe(6);
expect(libVizPanel.state.panelKey).toBe('panel-7');
expect(libVizPanel.state.panel?.state.key).toBe('panel-7');
expect(libVizPanel.state.key).toBe('panel-7');
});
it('Should duplicate a repeated panel', () => {
@ -855,19 +843,17 @@ describe('DashboardScene', () => {
((scene.state.body as SceneGridLayout).state.children[2] as SceneGridRow).state
.children[1] as DashboardGridItem
).state.body;
const vizPanel = (libraryPanel as LibraryVizPanel).state.panel;
scene.duplicatePanel(vizPanel as VizPanel);
scene.duplicatePanel(libraryPanel);
const body = scene.state.body as SceneGridLayout;
const gridRow = body.state.children[2] as SceneGridRow;
const gridItem = gridRow.state.children[2] as DashboardGridItem;
const libVizPanel = gridItem.state.body as LibraryVizPanel;
const libVizPanel = gridItem.state.body;
expect(gridRow.state.children.length).toBe(3);
expect(libVizPanel.state.panelKey).toBe('panel-7');
expect(libVizPanel.state.panel?.state.key).toBe('panel-7');
expect(libVizPanel.state.key).toBe('panel-7');
});
it('Should fail to duplicate a panel if it does not have a grid item parent', () => {
@ -886,16 +872,10 @@ describe('DashboardScene', () => {
});
it('Should unlink a library panel', () => {
const libPanel = new LibraryVizPanel({
title: 'title',
uid: 'abc',
name: 'lib panel',
panelKey: 'panel-1',
isLoaded: true,
panel: new VizPanel({
title: 'Panel B',
pluginId: 'table',
}),
const libPanel = new VizPanel({
title: 'Panel B',
pluginId: 'table',
$behaviors: [new LibraryPanelBehavior({ title: 'title', name: 'lib panel', uid: 'abc', isLoaded: true })],
});
const scene = buildTestScene({
@ -916,6 +896,7 @@ describe('DashboardScene', () => {
expect(body.state.children.length).toBe(1);
expect(gridItem.state.body).toBeInstanceOf(VizPanel);
expect(gridItem.state.$behaviors).toBeUndefined();
});
it('Should create a library panel', () => {
@ -945,11 +926,12 @@ describe('DashboardScene', () => {
const layout = scene.state.body as SceneGridLayout;
const newGridItem = layout.state.children[0] as DashboardGridItem;
const behavior = newGridItem.state.body.state.$behaviors![0] as LibraryPanelBehavior;
expect(layout.state.children.length).toBe(1);
expect(newGridItem.state.body).toBeInstanceOf(LibraryVizPanel);
expect((newGridItem.state.body as LibraryVizPanel).state.uid).toBe('uid');
expect((newGridItem.state.body as LibraryVizPanel).state.name).toBe('name');
expect(newGridItem.state.body).toBeInstanceOf(VizPanel);
expect(behavior.state.uid).toBe('uid');
expect(behavior.state.name).toBe('name');
});
it('Should create a library panel under a row', () => {
@ -984,12 +966,13 @@ describe('DashboardScene', () => {
const layout = scene.state.body as SceneGridLayout;
const newGridItem = (layout.state.children[0] as SceneGridRow).state.children[0] as DashboardGridItem;
const behavior = newGridItem.state.body.state.$behaviors![0] as LibraryPanelBehavior;
expect(layout.state.children.length).toBe(1);
expect((layout.state.children[0] as SceneGridRow).state.children.length).toBe(1);
expect(newGridItem.state.body).toBeInstanceOf(LibraryVizPanel);
expect((newGridItem.state.body as LibraryVizPanel).state.uid).toBe('uid');
expect((newGridItem.state.body as LibraryVizPanel).state.name).toBe('name');
expect(newGridItem.state.body).toBeInstanceOf(VizPanel);
expect(behavior.state.uid).toBe('uid');
expect(behavior.state.name).toBe('name');
});
});
});
@ -1266,16 +1249,11 @@ function buildTestScene(overrides?: Partial<DashboardSceneState>) {
}),
}),
new DashboardGridItem({
body: new LibraryVizPanel({
uid: 'uid',
name: 'libraryPanel',
panelKey: 'panel-5',
body: new VizPanel({
title: 'Library Panel',
panel: new VizPanel({
title: 'Library Panel',
key: 'panel-5',
pluginId: 'table',
}),
pluginId: 'table',
key: 'panel-5',
$behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })],
}),
}),
],
@ -1289,16 +1267,11 @@ function buildTestScene(overrides?: Partial<DashboardSceneState>) {
}),
}),
new DashboardGridItem({
body: new LibraryVizPanel({
uid: 'uid',
name: 'libraryPanel',
panelKey: 'panel-6',
body: new VizPanel({
title: 'Library Panel',
panel: new VizPanel({
title: 'Library Panel',
key: 'panel-6',
pluginId: 'table',
}),
pluginId: 'table',
key: 'panel-6',
$behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })],
}),
}),
],

View File

@ -42,17 +42,13 @@ import { ShowConfirmModalEvent } from 'app/types/events';
import { PanelEditor } from '../panel-edit/PanelEditor';
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer';
import {
buildGridItemForLibPanel,
buildGridItemForPanel,
transformSaveModelToScene,
} from '../serialization/transformSaveModelToScene';
import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
import { gridItemToPanel } from '../serialization/transformSceneToSaveModel';
import { DecoratedRevisionModel } from '../settings/VersionsEditView';
import { DashboardEditView } from '../settings/utils';
import { historySrv } from '../settings/version-history';
import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper';
import { dashboardSceneGraph, getLibraryVizPanelFromVizPanel } from '../utils/dashboardSceneGraph';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { djb2Hash } from '../utils/djb2Hash';
import { getDashboardUrl, getViewPanelUrl } from '../utils/urlBuilders';
import {
@ -73,7 +69,7 @@ import { DashboardControls } from './DashboardControls';
import { DashboardGridItem } from './DashboardGridItem';
import { DashboardSceneRenderer } from './DashboardSceneRenderer';
import { DashboardSceneUrlSync } from './DashboardSceneUrlSync';
import { LibraryVizPanel } from './LibraryVizPanel';
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
import { RowRepeaterBehavior } from './RowRepeaterBehavior';
import { ViewPanelScene } from './ViewPanelScene';
import { setupKeyboardShortcuts } from './keyboardShortcuts';
@ -541,13 +537,8 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
throw new Error('Trying to add a panel in a layout that is not SceneGridLayout');
}
const panelKey = panelToReplace.state.key;
const body = new LibraryVizPanel({
title: libPanel.model?.title ?? 'Panel',
uid: libPanel.uid,
name: libPanel.name,
panelKey: panelKey ?? getVizPanelKeyForPanelId(dashboardSceneGraph.getNextPanelId(this)),
const body = panelToReplace.clone({
$behaviors: [new LibraryPanelBehavior({ uid: libPanel.uid, name: libPanel.name })],
});
const gridItem = panelToReplace.parent;
@ -564,9 +555,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
return;
}
const libraryPanel = getLibraryVizPanelFromVizPanel(vizPanel);
const gridItem = libraryPanel ? libraryPanel.parent : vizPanel.parent;
const gridItem = vizPanel.parent;
if (!(gridItem instanceof DashboardGridItem)) {
console.error('Trying to duplicate a panel in a layout that is not DashboardGridItem');
@ -578,42 +567,28 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
let newGridItem;
const newPanelId = dashboardSceneGraph.getNextPanelId(this);
if (libraryPanel) {
const gridItemToDuplicateState = sceneUtils.cloneSceneObjectState(gridItem.state);
newGridItem = new DashboardGridItem({
x: gridItemToDuplicateState.x,
y: gridItemToDuplicateState.y,
width: gridItemToDuplicateState.width,
height: gridItemToDuplicateState.height,
body: new LibraryVizPanel({
title: libraryPanel.state.title,
uid: libraryPanel.state.uid,
name: libraryPanel.state.name,
panelKey: getVizPanelKeyForPanelId(newPanelId),
}),
});
if (gridItem instanceof DashboardGridItem) {
panelState = sceneUtils.cloneSceneObjectState(gridItem.state.body.state);
panelData = sceneGraph.getData(gridItem.state.body).clone();
} else {
if (gridItem instanceof DashboardGridItem) {
panelState = sceneUtils.cloneSceneObjectState(gridItem.state.body.state);
panelData = sceneGraph.getData(gridItem.state.body).clone();
} else {
panelState = sceneUtils.cloneSceneObjectState(vizPanel.state);
panelData = sceneGraph.getData(vizPanel).clone();
}
// when we duplicate a panel we don't want to clone the alert state
delete panelData.state.data?.alertState;
newGridItem = new DashboardGridItem({
x: gridItem.state.x,
y: gridItem.state.y,
height: gridItem.state.height,
width: gridItem.state.width,
body: new VizPanel({ ...panelState, $data: panelData, key: getVizPanelKeyForPanelId(newPanelId) }),
});
panelState = sceneUtils.cloneSceneObjectState(vizPanel.state);
panelData = sceneGraph.getData(vizPanel).clone();
}
// when we duplicate a panel we don't want to clone the alert state
delete panelData.state.data?.alertState;
newGridItem = new DashboardGridItem({
x: gridItem.state.x,
y: gridItem.state.y,
height: gridItem.state.height,
width: gridItem.state.width,
variableName: gridItem.state.variableName,
repeatDirection: gridItem.state.repeatDirection,
maxPerRow: gridItem.state.maxPerRow,
body: new VizPanel({ ...panelState, $data: panelData, key: getVizPanelKeyForPanelId(newPanelId) }),
});
if (!(this.state.body instanceof SceneGridLayout)) {
console.error('Trying to duplicate a panel in a layout that is not SceneGridLayout ');
return;
@ -645,16 +620,6 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
let gridItem = vizPanel.parent;
if (vizPanel.parent instanceof LibraryVizPanel) {
const libraryVizPanel = vizPanel.parent;
if (!libraryVizPanel.parent) {
return;
}
gridItem = libraryVizPanel.parent;
}
if (!(gridItem instanceof DashboardGridItem)) {
console.error('Trying to copy a panel that is not DashboardGridItem child');
throw new Error('Trying to copy a panel that is not DashboardGridItem child');
@ -674,9 +639,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
const jsonData = store.get(LS_PANEL_COPY_KEY);
const jsonObj = JSON.parse(jsonData);
const panelModel = new PanelModel(jsonObj);
const gridItem = !panelModel.libraryPanel
? buildGridItemForPanel(panelModel)
: buildGridItemForLibPanel(panelModel);
const gridItem = buildGridItemForPanel(panelModel);
const sceneGridLayout = this.state.body;
@ -686,25 +649,9 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
const panelId = dashboardSceneGraph.getNextPanelId(this);
if (gridItem instanceof DashboardGridItem && gridItem.state.body instanceof LibraryVizPanel) {
const panelKey = getVizPanelKeyForPanelId(panelId);
gridItem.state.body.setState({ panelKey });
const vizPanel = gridItem.state.body.state.panel;
if (vizPanel instanceof VizPanel) {
vizPanel.setState({ key: panelKey });
}
} else if (gridItem instanceof DashboardGridItem && gridItem.state.body) {
gridItem.state.body.setState({
key: getVizPanelKeyForPanelId(panelId),
});
} else if (gridItem instanceof DashboardGridItem) {
gridItem.state.body.setState({
key: getVizPanelKeyForPanelId(panelId),
});
}
gridItem.state.body.setState({
key: getVizPanelKeyForPanelId(panelId),
});
gridItem.setState({
height: NEW_PANEL_HEIGHT,
@ -723,7 +670,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
public removePanel(panel: VizPanel) {
const panels: SceneObject[] = [];
const key = panel.parent instanceof LibraryVizPanel ? panel.parent.parent?.state.key : panel.parent?.state.key;
const key = panel.parent?.state.key;
if (!key) {
return;
@ -764,7 +711,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
}
}
public unlinkLibraryPanel(panel: LibraryVizPanel) {
public unlinkLibraryPanel(panel: VizPanel) {
if (!panel.parent) {
return;
}
@ -772,13 +719,11 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
const gridItem = panel.parent;
if (!(gridItem instanceof DashboardGridItem)) {
console.error('Trying to unlinka a lib panel in a layout that is not DashboardGridItem');
console.error('Trying to unlink a lib panel in a layout that is not DashboardGridItem');
return;
}
gridItem?.setState({
body: panel.state.panel?.clone(),
});
gridItem.state.body.setState({ $behaviors: undefined });
}
public showModal(modal: SceneObject) {
@ -812,7 +757,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
locationService.partial({ editview: 'settings' });
};
public onShowAddLibraryPanelDrawer(panelToReplaceRef?: SceneObjectRef<LibraryVizPanel>) {
public onShowAddLibraryPanelDrawer(panelToReplaceRef?: SceneObjectRef<VizPanel>) {
this.setState({
overlay: new AddLibraryPanelDrawer({ panelToReplaceRef }),
});

View File

@ -21,13 +21,13 @@ import { ShareModal } from '../sharing/ShareModal';
import {
findVizPanelByKey,
getDashboardSceneFor,
getLibraryPanel,
getLibraryPanelBehavior,
isPanelClone,
isWithinUnactivatedRepeatRow,
} from '../utils/utils';
import { DashboardScene, DashboardSceneState } from './DashboardScene';
import { LibraryVizPanel } from './LibraryVizPanel';
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
import { ViewPanelScene } from './ViewPanelScene';
import { DashboardRepeatsProcessedEvent } from './types';
@ -83,20 +83,6 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
return;
}
if (getLibraryPanel(panel)) {
this._handleLibraryPanel(panel, (p) => {
if (p.state.key === undefined) {
// Inspect drawer require a panel key to be set
throw new Error('library panel key is undefined');
}
const drawer = new PanelInspectDrawer({
$behaviors: [new ResolveInspectPanelByKey({ panelKey: p.state.key })],
});
this._scene.setState({ overlay: drawer, inspectPanelKey: p.state.key });
});
return;
}
update.inspectPanelKey = values.inspect;
update.overlay = new PanelInspectDrawer({
$behaviors: [new ResolveInspectPanelByKey({ panelKey: values.inspect })],
@ -122,11 +108,6 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
return;
}
if (getLibraryPanel(panel)) {
this._handleLibraryPanel(panel, (p) => this._buildLibraryPanelViewScene(p));
return;
}
if (isWithinUnactivatedRepeatRow(panel)) {
this._handleViewRepeatClone(values.viewPanel);
return;
@ -155,10 +136,10 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
if (!isEditing) {
this._scene.onEnterEditMode();
}
if (getLibraryPanel(panel)) {
this._handleLibraryPanel(panel, (p) => {
this._scene.setState({ editPanel: buildPanelEditScene(p) });
});
const libPanelBehavior = getLibraryPanelBehavior(panel);
if (libPanelBehavior && !libPanelBehavior?.state.isLoaded) {
this._waitForLibPanelToLoadBeforeEnteringPanelEdit(panel, libPanelBehavior);
return;
}
@ -202,25 +183,6 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
}
}
private _buildLibraryPanelViewScene(vizPanel: VizPanel) {
this._scene.setState({ viewPanelScene: new ViewPanelScene({ panelRef: vizPanel.getRef() }) });
}
private _handleLibraryPanel(vizPanel: VizPanel, cb: (p: VizPanel) => void): void {
if (!(vizPanel.parent instanceof LibraryVizPanel)) {
throw new Error('Panel is not a child of a LibraryVizPanel');
}
const libraryPanel = vizPanel.parent;
if (libraryPanel.state.isLoaded) {
cb(vizPanel);
} else {
libraryPanel.subscribeToState((n) => {
cb(n.panel!);
});
libraryPanel.activate();
}
}
private _handleViewRepeatClone(viewPanel: string) {
if (!this._eventSub) {
this._eventSub = this._scene.subscribeToEvent(DashboardRepeatsProcessedEvent, () => {
@ -232,6 +194,18 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
});
}
}
/**
* Temporary solution, with some refactoring of PanelEditor we can remove this
*/
private _waitForLibPanelToLoadBeforeEnteringPanelEdit(panel: VizPanel, libPanel: LibraryPanelBehavior) {
const sub = libPanel.subscribeToState((state) => {
if (state.isLoaded) {
this._scene.setState({ editPanel: buildPanelEditScene(panel) });
sub.unsubscribe();
}
});
}
}
interface ResolveInspectPanelByKeyState extends SceneObjectState {

View File

@ -0,0 +1,183 @@
import { of } from 'rxjs';
import { FieldType, LoadingState, PanelData, getDefaultTimeRange, toDataFrame } from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { setPluginImportUtils, setRunRequest } from '@grafana/runtime';
import { SceneCanvasText, SceneGridLayout, VizPanel } from '@grafana/scenes';
import { LibraryPanel } from '@grafana/schema';
import * as libpanels from 'app/features/library-panels/state/api';
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
import { activateFullSceneTree } from '../utils/test-utils';
import { DashboardGridItem } from './DashboardGridItem';
import { DashboardScene } from './DashboardScene';
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
setPluginImportUtils({
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
getPanelPluginFromCache: (id: string) => undefined,
});
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
setPluginExtensionGetter: jest.fn(),
getPluginLinkExtensions: jest.fn(() => ({
extensions: [],
})),
getDataSourceSrv: () => {
return {
get: jest.fn().mockResolvedValue({
getRef: () => ({ uid: 'ds1' }),
}),
getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds1' }),
};
},
}));
const runRequestMock = jest.fn().mockReturnValue(
of<PanelData>({
state: LoadingState.Done,
timeRange: getDefaultTimeRange(),
series: [
toDataFrame({
fields: [{ name: 'value', type: FieldType.number, values: [1, 2, 3] }],
}),
],
request: {
app: 'dashboard',
requestId: 'request-id',
dashboardUID: 'asd',
interval: '1s',
panelId: 1,
range: getDefaultTimeRange(),
targets: [],
timezone: 'utc',
intervalMs: 1000,
startTime: 1,
scopedVars: {
__sceneObject: { value: new SceneCanvasText({ text: 'asd' }) },
},
},
})
);
setRunRequest(runRequestMock);
describe('LibraryPanelBehavior', () => {
it('should load library panel', async () => {
const { gridItem, spy, behavior } = await buildTestSceneWithLibraryPanel();
expect(behavior.state.isLoaded).toBe(true);
expect(behavior.state._loadedPanel).toBeDefined();
expect(behavior.state._loadedPanel?.model).toBeDefined();
expect(behavior.state._loadedPanel?.name).toBe('LibraryPanel A');
expect(behavior.state._loadedPanel?.type).toBe('table');
// Verify the viz panel state have been updated with lib panel options
expect(gridItem.state.body.state.options).toEqual({ showHeader: true });
expect(spy).toHaveBeenCalled();
});
it('should not update panel if version is the same', async () => {
const { gridItem } = await buildTestSceneWithLibraryPanel();
const behavior = gridItem.state.body.state.$behaviors![0] as LibraryPanelBehavior;
expect(behavior).toBeDefined();
const panel = vizPanelToPanel(gridItem.state.body.clone({ $behaviors: undefined }));
const libraryPanelState = {
name: 'LibraryPanel B',
title: 'LibraryPanel B title',
uid: '222',
type: 'table',
version: 1,
model: panel,
};
behavior.setPanelFromLibPanel(libraryPanelState);
expect(behavior.state._loadedPanel?.name).toBe('LibraryPanel A');
expect(behavior.state._loadedPanel?.uid).toBe('111');
});
it('should not update panel if behavior not part of a vizPanel', async () => {
const { gridItem } = await buildTestSceneWithLibraryPanel();
const behavior = gridItem.state.body.state.$behaviors![0] as LibraryPanelBehavior;
expect(behavior).toBeDefined();
const panel = vizPanelToPanel(gridItem.state.body.clone({ $behaviors: undefined }));
const libraryPanelState = {
name: 'LibraryPanel B',
title: 'LibraryPanel B title',
uid: '222',
type: 'table',
version: 2,
model: panel,
};
const behaviorClone = behavior.clone();
behaviorClone.setPanelFromLibPanel(libraryPanelState);
expect(behaviorClone.state._loadedPanel?.name).toBe('LibraryPanel A');
expect(behaviorClone.state._loadedPanel?.uid).toBe('111');
});
});
async function buildTestSceneWithLibraryPanel() {
const behavior = new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: '111' });
const vizPanel = new VizPanel({
title: 'Panel A',
pluginId: 'lib-panel-loading',
key: 'panel-1',
$behaviors: [behavior],
});
const libraryPanel: LibraryPanel = {
name: 'LibraryPanel A',
uid: '111',
type: 'table',
model: {
title: 'LibraryPanel A title',
type: 'table',
options: { showHeader: true },
fieldConfig: { defaults: {}, overrides: [] },
datasource: { uid: 'abcdef' },
targets: [{ refId: 'A' }],
},
version: 1,
};
const spy = jest.spyOn(libpanels, 'getLibraryPanel').mockResolvedValue(libraryPanel);
const gridItem = new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: vizPanel,
});
const scene = new DashboardScene({
title: 'hello',
uid: 'dash-1',
meta: {
canEdit: true,
},
body: new SceneGridLayout({
children: [gridItem],
}),
});
activateFullSceneTree(scene);
await new Promise((r) => setTimeout(r, 1));
return { scene, gridItem, spy, behavior };
}

View File

@ -0,0 +1,111 @@
import { PanelPlugin, PanelProps } from '@grafana/data';
import { SceneObjectBase, SceneObjectState, sceneUtils, VizPanel, VizPanelState } from '@grafana/scenes';
import { LibraryPanel } from '@grafana/schema';
import { Stack } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { PanelModel } from 'app/features/dashboard/state';
import { getLibraryPanel } from 'app/features/library-panels/state/api';
import { createPanelDataProvider } from '../utils/createPanelDataProvider';
import { DashboardGridItem } from './DashboardGridItem';
interface LibraryPanelBehaviorState 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;
isLoaded?: boolean;
_loadedPanel?: LibraryPanel;
}
export class LibraryPanelBehavior extends SceneObjectBase<LibraryPanelBehaviorState> {
public static LOADING_VIZ_PANEL_PLUGIN_ID = 'library-panel-loading-plugin';
public constructor(state: LibraryPanelBehaviorState) {
super(state);
this.addActivationHandler(() => this._activationHandler());
}
private _activationHandler() {
if (!this.state.isLoaded) {
this.loadLibraryPanelFromPanelModel();
}
}
public setPanelFromLibPanel(libPanel: LibraryPanel) {
if (this.state._loadedPanel?.version === libPanel.version) {
return;
}
const vizPanel = this.parent;
if (!(vizPanel instanceof VizPanel)) {
return;
}
const libPanelModel = new PanelModel(libPanel.model);
const vizPanelState: VizPanelState = {
title: libPanelModel.title,
options: libPanelModel.options ?? {},
fieldConfig: libPanelModel.fieldConfig,
pluginId: libPanelModel.type,
pluginVersion: libPanelModel.pluginVersion,
displayMode: libPanelModel.transparent ? 'transparent' : undefined,
description: libPanelModel.description,
$data: createPanelDataProvider(libPanelModel),
};
vizPanel.setState(vizPanelState);
vizPanel.changePluginType(libPanelModel.type, vizPanelState.options, vizPanelState.fieldConfig);
this.setState({ _loadedPanel: libPanel, isLoaded: true, name: libPanel.name, title: libPanelModel.title });
const layoutElement = vizPanel.parent!;
// Migrate repeat options to layout element
if (libPanelModel.repeat && layoutElement instanceof DashboardGridItem) {
layoutElement.setState({
variableName: libPanelModel.repeat,
repeatDirection: libPanelModel.repeatDirection === 'h' ? 'h' : 'v',
maxPerRow: libPanelModel.maxPerRow,
itemHeight: layoutElement.state.height ?? 10,
});
layoutElement.performRepeat();
}
}
private async loadLibraryPanelFromPanelModel() {
let vizPanel = this.parent;
if (!(vizPanel instanceof VizPanel)) {
return;
}
try {
const libPanel = await getLibraryPanel(this.state.uid, true);
this.setPanelFromLibPanel(libPanel);
} catch (err) {
vizPanel.setState({
_pluginLoadError: `Unable to load library panel: ${this.state.uid}`,
});
}
}
}
const LoadingVizPanelPlugin = new PanelPlugin(LoadingVizPanel);
function LoadingVizPanel(props: PanelProps) {
return (
<Stack direction={'column'} justifyContent={'space-between'}>
<Trans i18nKey="library-panels.loading-panel-text">Loading library panel</Trans>
</Stack>
);
}
sceneUtils.registerRuntimePanelPlugin({
pluginId: LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID,
plugin: LoadingVizPanelPlugin,
});

View File

@ -1,140 +0,0 @@
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 { SceneGridLayout, VizPanel } from '@grafana/scenes';
import { LibraryPanel } from '@grafana/schema';
import { backendSrv } from 'app/core/services/backend_srv';
import { DashboardGridItem } from './DashboardGridItem';
import { LibraryVizPanel } from './LibraryVizPanel';
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 configure repeat options of DashboardGridIem 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 DashboardGridItem({ body: libVizPanel })],
});
layout.activate();
libVizPanel.activate();
await waitFor(() => {
expect(layout.state.children[0]).toBeInstanceOf(DashboardGridItem);
expect((layout.state.children[0] as DashboardGridItem).state.variableName).toBe('query0');
expect((layout.state.children[0] as DashboardGridItem).state.repeatDirection).toBe('h');
expect((layout.state.children[0] as DashboardGridItem).state.maxPerRow).toBe(4);
});
});
});
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: 'Dashboards',
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,124 +0,0 @@
import {
SceneComponentProps,
SceneObjectBase,
SceneObjectState,
VizPanel,
VizPanelMenu,
VizPanelState,
} from '@grafana/scenes';
import { LibraryPanel } from '@grafana/schema';
import { PanelModel } from 'app/features/dashboard/state';
import { getLibraryPanel } from 'app/features/library-panels/state/api';
import { createPanelDataProvider } from '../utils/createPanelDataProvider';
import { DashboardGridItem } from './DashboardGridItem';
import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks';
import { panelLinksBehavior, panelMenuBehavior } from './PanelMenuBehavior';
import { PanelNotices } from './PanelNotices';
export 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;
isLoaded?: boolean;
panelKey: string;
_loadedPanel?: LibraryPanel;
}
export class LibraryVizPanel extends SceneObjectBase<LibraryVizPanelState> {
static Component = LibraryPanelRenderer;
constructor(state: LibraryVizPanelState) {
super({
panel: state.panel ?? getLoadingPanel(state.title, state.panelKey),
isLoaded: state.isLoaded ?? false,
...state,
});
this.addActivationHandler(this._onActivate);
}
private _onActivate = () => {
if (!this.state.isLoaded) {
this.loadLibraryPanelFromPanelModel();
}
};
public setPanelFromLibPanel(libPanel: LibraryPanel) {
if (this.state._loadedPanel?.version === libPanel.version) {
return;
}
const libPanelModel = new PanelModel(libPanel.model);
const vizPanelState: VizPanelState = {
title: libPanelModel.title,
key: this.state.panelKey,
options: libPanelModel.options ?? {},
fieldConfig: libPanelModel.fieldConfig,
pluginId: libPanelModel.type,
pluginVersion: libPanelModel.pluginVersion,
displayMode: libPanelModel.transparent ? 'transparent' : undefined,
description: libPanelModel.description,
// To be replaced with it's own option persisted option instead derived
hoverHeader: !libPanelModel.title && !libPanelModel.timeFrom && !libPanelModel.timeShift,
hoverHeaderOffset: 0,
$data: createPanelDataProvider(libPanelModel),
menu: new VizPanelMenu({ $behaviors: [panelMenuBehavior] }),
titleItems: [
new VizPanelLinks({
rawLinks: libPanelModel.links,
menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }),
}),
new PanelNotices(),
],
};
const panel = new VizPanel(vizPanelState);
this.setState({ panel, _loadedPanel: libPanel, isLoaded: true, name: libPanel.name });
}
private async loadLibraryPanelFromPanelModel() {
let vizPanel = this.state.panel!;
try {
const libPanel = await getLibraryPanel(this.state.uid, true);
this.setPanelFromLibPanel(libPanel);
if (this.parent instanceof DashboardGridItem) {
this.parent.setState({
variableName: libPanel.model.repeat,
repeatDirection: libPanel.model.repeatDirection === 'h' ? 'h' : 'v',
maxPerRow: libPanel.model.maxPerRow,
});
}
} catch (err) {
vizPanel.setState({
_pluginLoadError: `Unable to load library panel: ${this.state.uid}`,
});
}
}
}
function getLoadingPanel(title: string, panelKey: string) {
return new VizPanel({
key: panelKey,
title,
menu: new VizPanelMenu({
$behaviors: [panelMenuBehavior],
}),
});
}
function LibraryPanelRenderer({ model }: SceneComponentProps<LibraryVizPanel>) {
const { panel } = model.useState();
if (!panel) {
return null;
}
return <panel.Component model={panel} />;
}

View File

@ -30,10 +30,10 @@ import ExportButton from '../sharing/ExportButton/ExportButton';
import ShareButton from '../sharing/ShareButton/ShareButton';
import { DashboardInteractions } from '../utils/interactions';
import { DynamicDashNavButtonModel, dynamicDashNavActions } from '../utils/registerDynamicDashNavAction';
import { isLibraryPanel } from '../utils/utils';
import { DashboardScene } from './DashboardScene';
import { GoToSnapshotOriginButton } from './GoToSnapshotOriginButton';
import { LibraryVizPanel } from './LibraryVizPanel';
interface Props {
dashboard: DashboardScene;
@ -619,7 +619,7 @@ function useEditingLibraryPanel(panelEditor?: PanelEditor) {
useEffect(() => {
if (panelEditor) {
const unsub = panelEditor.state.vizManager.subscribeToState((vizManagerState) =>
setEditingLibraryPanel(vizManagerState.sourcePanel.resolve().parent instanceof LibraryVizPanel)
setEditingLibraryPanel(isLibraryPanel(vizManagerState.sourcePanel.resolve()))
);
return () => {
unsub.unsubscribe();

View File

@ -26,10 +26,9 @@ import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
import { ShareModal } from '../sharing/ShareModal';
import { DashboardInteractions } from '../utils/interactions';
import { getEditPanelUrl, getInspectUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders';
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils';
import { DashboardScene } from './DashboardScene';
import { LibraryVizPanel } from './LibraryVizPanel';
import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks';
import { UnlinkLibraryPanelModal } from './UnlinkLibraryPanelModal';
@ -41,7 +40,6 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
// hm.. add another generic param to SceneObject to specify parent type?
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const panel = menu.parent as VizPanel;
const parent = panel.parent;
const plugin = panel.getPlugin();
const items: PanelMenuItem[] = [];
@ -164,13 +162,13 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
}
if (dashboard.state.isEditing && !isRepeat && !isEditingPanel) {
if (parent instanceof LibraryVizPanel) {
if (isLibraryPanel(panel)) {
moreSubMenu.push({
text: t('panel.header-menu.unlink-library-panel', `Unlink library panel`),
onClick: () => {
dashboard.showModal(
new UnlinkLibraryPanelModal({
panelRef: parent.getRef(),
panelRef: panel.getRef(),
})
);
},
@ -179,7 +177,7 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
moreSubMenu.push({
text: t('panel.header-menu.replace-library-panel', `Replace library panel`),
onClick: () => {
dashboard.onShowAddLibraryPanelDrawer(parent.getRef());
dashboard.onShowAddLibraryPanelDrawer(panel.getRef());
},
});
} else {

View File

@ -1,13 +1,12 @@
import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState } from '@grafana/scenes';
import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState, VizPanel } from '@grafana/scenes';
import { ModalSceneObjectLike } from '../sharing/types';
import { getDashboardSceneFor } from '../utils/utils';
import { LibraryVizPanel } from './LibraryVizPanel';
import { UnlinkModal } from './UnlinkModal';
interface UnlinkLibraryPanelModalState extends SceneObjectState {
panelRef?: SceneObjectRef<LibraryVizPanel>;
panelRef?: SceneObjectRef<VizPanel>;
}
export class UnlinkLibraryPanelModal

View File

@ -28,7 +28,7 @@ import { DashboardDataDTO } from 'app/types';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { PanelTimeRange } from '../scene/PanelTimeRange';
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
import { NEW_LINK } from '../settings/links/utils';
@ -44,7 +44,6 @@ import {
buildGridItemForPanel,
transformSaveModelToScene,
convertOldSnapshotToScenesSnapshot,
buildGridItemForLibPanel,
} from './transformSaveModelToScene';
describe('transformSaveModelToScene', () => {
@ -388,7 +387,9 @@ describe('transformSaveModelToScene', () => {
expect(rowScene.state.children[0]).toBeInstanceOf(DashboardGridItem);
expect(rowScene.state.children[1]).toBeInstanceOf(DashboardGridItem);
// Panels are sorted by position in the row
expect((rowScene.state.children[0] as DashboardGridItem).state.body!).toBeInstanceOf(LibraryVizPanel);
expect((rowScene.state.children[0] as DashboardGridItem).state.body.state.$behaviors![0]).toBeInstanceOf(
LibraryPanelBehavior
);
expect((rowScene.state.children[1] as DashboardGridItem).state.body!).toBeInstanceOf(VizPanel);
});
@ -481,7 +482,7 @@ describe('transformSaveModelToScene', () => {
// lib panel out of row
expect(body.state.children[1]).toBeInstanceOf(DashboardGridItem);
const panelOutOfRowLibVizPanel = body.state.children[1] as DashboardGridItem;
expect(panelOutOfRowLibVizPanel.state.body!).toBeInstanceOf(LibraryVizPanel);
expect(panelOutOfRowLibVizPanel.state.body.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior);
// Row with panels
expect(body.state.children[2]).toBeInstanceOf(SceneGridRow);
const rowWithPanelsScene = body.state.children[2] as SceneGridRow;
@ -489,7 +490,7 @@ describe('transformSaveModelToScene', () => {
expect(rowWithPanelsScene.state.key).toBe('panel-10');
expect(rowWithPanelsScene.state.children).toHaveLength(2);
const libPanel = rowWithPanelsScene.state.children[1] as DashboardGridItem;
expect(libPanel.state.body!).toBeInstanceOf(LibraryVizPanel);
expect(libPanel.state.body.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior);
// Panel within row
expect(rowWithPanelsScene.state.children[0]).toBeInstanceOf(DashboardGridItem);
const panelInRowVizPanel = rowWithPanelsScene.state.children[0] as DashboardGridItem;
@ -721,7 +722,7 @@ describe('transformSaveModelToScene', () => {
expect(runner.state.queryCachingTTL).toBe(200000);
});
it('should convert saved lib panel to LibraryVizPanel', () => {
it('should convert saved lib panel to a viz panel with LibraryPanelBehavior', () => {
const panel = {
title: 'Panel',
gridPos: { x: 0, y: 0, w: 12, h: 8 },
@ -733,12 +734,13 @@ describe('transformSaveModelToScene', () => {
},
};
const gridItem = buildGridItemForLibPanel(new PanelModel(panel))!;
const libVizPanel = gridItem.state.body as LibraryVizPanel;
const gridItem = buildGridItemForPanel(new PanelModel(panel))!;
const libPanelBehavior = gridItem.state.body.state.$behaviors![0];
expect(libVizPanel.state.uid).toEqual(panel.libraryPanel.uid);
expect(libVizPanel.state.name).toEqual(panel.libraryPanel.name);
expect(libVizPanel.state.title).toEqual(panel.title);
expect(libPanelBehavior).toBeInstanceOf(LibraryPanelBehavior);
expect((libPanelBehavior as LibraryPanelBehavior).state.uid).toEqual(panel.libraryPanel.uid);
expect((libPanelBehavior as LibraryPanelBehavior).state.name).toEqual(panel.libraryPanel.name);
expect(gridItem.state.body.state.title).toEqual(panel.title);
});
});

View File

@ -32,7 +32,7 @@ import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardGridItem, RepeatDirection } from '../scene/DashboardGridItem';
import { registerDashboardMacro } from '../scene/DashboardMacro';
import { DashboardScene } from '../scene/DashboardScene';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavior';
import { PanelNotices } from '../scene/PanelNotices';
@ -107,18 +107,6 @@ export function createSceneObjectsForPanels(oldPanels: PanelModel[]): SceneGridI
currentRowPanels = [];
}
}
} else if (panel.libraryPanel?.uid && !('model' in panel.libraryPanel)) {
const gridItem = buildGridItemForLibPanel(panel);
if (!gridItem) {
continue;
}
if (currentRow) {
currentRowPanels.push(gridItem);
} else {
panels.push(gridItem);
}
} else {
// when rendering a snapshot created with the legacy Dashboards convert data to new snapshot format to be compatible with Scenes
if (panel.snapshotData) {
@ -153,16 +141,6 @@ function createRowFromPanelModel(row: PanelModel, content: SceneGridItemLike[]):
saveModel = new PanelModel(saveModel);
}
if (saveModel.libraryPanel?.uid && !('model' in saveModel.libraryPanel)) {
const gridItem = buildGridItemForLibPanel(saveModel);
if (!gridItem) {
throw new Error('Failed to build grid item for library panel');
}
return gridItem;
}
return buildGridItemForPanel(saveModel);
});
}
@ -287,29 +265,6 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
return dashboardScene;
}
export function buildGridItemForLibPanel(panel: PanelModel) {
if (!panel.libraryPanel) {
return null;
}
const body = new LibraryVizPanel({
title: panel.title,
uid: panel.libraryPanel.uid,
name: panel.libraryPanel.name,
panelKey: getVizPanelKeyForPanelId(panel.id),
});
return new DashboardGridItem({
key: `grid-item-${panel.id}`,
y: panel.gridPos.y,
x: panel.gridPos.x,
width: panel.gridPos.w,
height: panel.gridPos.h,
itemHeight: panel.gridPos.h,
body,
});
}
export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem {
const repeatOptions: Partial<{ variableName: string; repeatDirection: RepeatDirection }> = panel.repeat
? {
@ -333,7 +288,7 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem {
key: getVizPanelKeyForPanelId(panel.id),
title: panel.title,
description: panel.description,
pluginId: panel.type,
pluginId: panel.type ?? 'timeseries',
options: panel.options ?? {},
fieldConfig: panel.fieldConfig,
pluginVersion: panel.pluginVersion,
@ -343,11 +298,19 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem {
hoverHeaderOffset: 0,
$data: createPanelDataProvider(panel),
titleItems,
$behaviors: [],
extendPanelContext: setDashboardPanelContext,
_UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel),
};
if (panel.libraryPanel) {
vizPanelState.$behaviors!.push(
new LibraryPanelBehavior({ uid: panel.libraryPanel.uid, name: panel.libraryPanel.name })
);
vizPanelState.pluginId = LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID;
vizPanelState.$data = undefined;
}
if (!config.publicDashboardAccessToken) {
vizPanelState.menu = new VizPanelMenu({
$behaviors: [panelMenuBehavior],

View File

@ -33,7 +33,7 @@ import { buildPanelEditScene } from '../panel-edit/PanelEditor';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene } from '../scene/DashboardScene';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
import { NEW_LINK } from '../settings/links/utils';
import { activateFullSceneTree, buildPanelRepeaterScene } from '../utils/test-utils';
@ -44,11 +44,7 @@ import dashboard_to_load1 from './testfiles/dashboard_to_load1.json';
import repeatingRowsAndPanelsDashboardJson from './testfiles/repeating_rows_and_panels.json';
import snapshotableDashboardJson from './testfiles/snapshotable_dashboard.json';
import snapshotableWithRowsDashboardJson from './testfiles/snapshotable_with_rows.json';
import {
buildGridItemForLibPanel,
buildGridItemForPanel,
transformSaveModelToScene,
} from './transformSaveModelToScene';
import { buildGridItemForPanel, transformSaveModelToScene } from './transformSaveModelToScene';
import {
gridItemToPanel,
gridRowToSaveModel,
@ -348,33 +344,33 @@ describe('transformSceneToSaveModel', () => {
describe('Library panels', () => {
it('given a library panel', () => {
// Not using buildGridItemFromPanelSchema since it strips options/fieldConfig
const libVizPanel = new LibraryVizPanel({
name: 'Some lib panel panel',
title: 'A panel',
uid: 'lib-panel-uid',
panelKey: 'lib-panel',
panel: new VizPanel({
key: 'panel-4',
title: 'Panel blahh blah',
fieldConfig: {
defaults: {},
overrides: [],
const libVizPanel = new VizPanel({
key: 'panel-4',
title: 'Panel blahh blah',
$behaviors: [
new LibraryPanelBehavior({
name: 'Some lib panel panel',
title: 'A panel',
uid: 'lib-panel-uid',
}),
],
fieldConfig: {
defaults: {},
overrides: [],
},
options: {
legend: {
calcs: [],
displayMode: 'list',
placement: 'bottom',
showLegend: true,
},
options: {
legend: {
calcs: [],
displayMode: 'list',
placement: 'bottom',
showLegend: true,
},
tooltip: {
maxHeight: 600,
mode: 'single',
sort: 'none',
},
tooltip: {
maxHeight: 600,
mode: 'single',
sort: 'none',
},
}),
},
});
const panel = new DashboardGridItem({
@ -824,32 +820,33 @@ describe('transformSceneToSaveModel', () => {
it('handles repeated library panels', () => {
const { scene, repeater } = buildPanelRepeaterScene(
{ variableQueryTime: 0, numberOfOptions: 2 },
new LibraryVizPanel({
name: 'Some lib panel panel',
title: 'A panel',
uid: 'lib-panel-uid',
panelKey: 'lib-panel',
panel: new VizPanel({
key: 'panel-4',
title: 'Panel blahh blah',
fieldConfig: {
defaults: {},
overrides: [],
new VizPanel({
key: 'panel-4',
title: 'Panel blahh blah',
fieldConfig: {
defaults: {},
overrides: [],
},
options: {
legend: {
calcs: [],
displayMode: 'list',
placement: 'bottom',
showLegend: true,
},
options: {
legend: {
calcs: [],
displayMode: 'list',
placement: 'bottom',
showLegend: true,
},
tooltip: {
maxHeight: 600,
mode: 'single',
sort: 'none',
},
tooltip: {
maxHeight: 600,
mode: 'single',
sort: 'none',
},
}),
},
$behaviors: [
new LibraryPanelBehavior({
name: 'Some lib panel panel',
title: 'A panel',
uid: 'lib-panel-uid',
}),
],
})
);
@ -1138,9 +1135,5 @@ describe('transformSceneToSaveModel', () => {
});
export function buildGridItemFromPanelSchema(panel: Partial<Panel>) {
if (panel.libraryPanel) {
return buildGridItemForLibPanel(new PanelModel(panel))!;
}
return buildGridItemForPanel(new PanelModel(panel));
}

View File

@ -34,11 +34,10 @@ import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { PanelTimeRange } from '../scene/PanelTimeRange';
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
import { getLibraryPanelBehavior, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils';
import { GRAFANA_DATASOURCE_REF } from './const';
import { dataLayersToAnnotations } from './dataLayersToAnnotations';
@ -140,22 +139,6 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
return sortedDeepCloneWithoutNulls(dashboard);
}
export function libraryVizPanelToPanel(libPanel: LibraryVizPanel, gridPos: GridPos): Panel {
if (!libPanel.state.panel) {
throw new Error('Library panel has no panel');
}
return {
id: getPanelIdForVizPanel(libPanel.state.panel),
title: libPanel.state.title,
gridPos: gridPos,
libraryPanel: {
name: libPanel.state.name,
uid: libPanel.state.uid,
},
} as Panel;
}
export function gridItemToPanel(
gridItem: DashboardGridItem,
sceneState?: DashboardSceneState,
@ -167,16 +150,6 @@ export function gridItemToPanel(
w = 0,
h = 0;
// Handle library panels, early exit
if (gridItem.state.body instanceof LibraryVizPanel) {
x = gridItem.state.x ?? 0;
y = gridItem.state.y ?? 0;
w = gridItem.state.width ?? 0;
h = gridItem.state.itemHeight ?? gridItem.state.height ?? 0;
return libraryVizPanelToPanel(gridItem.state.body, { x, y, w, h });
}
let gridItem_ = gridItem;
// If we're saving while the panel editor is open, we need to persist those changes in the panel model
@ -186,7 +159,7 @@ export function gridItemToPanel(
sceneState.editPanel.state.vizManager.state.sourcePanel.resolve() === gridItem.state.body
) {
const gridItemClone = gridItem.clone();
if (gridItemClone.state.body instanceof VizPanel) {
if (gridItemClone.state.body instanceof VizPanel && !isLibraryPanel(gridItemClone.state.body)) {
sceneState.editPanel.state.vizManager.commitChangesTo(gridItemClone.state.body);
gridItem_ = gridItemClone;
}
@ -217,18 +190,36 @@ export function vizPanelToPanel(
isSnapshot = false,
gridItem?: SceneGridItemLike
) {
const panel: Panel = {
id: getPanelIdForVizPanel(vizPanel),
type: vizPanel.state.pluginId,
title: vizPanel.state.title,
description: vizPanel.state.description ?? undefined,
gridPos,
fieldConfig: (vizPanel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] },
transformations: [],
transparent: vizPanel.state.displayMode === 'transparent',
pluginVersion: vizPanel.state.pluginVersion,
...vizPanelDataToPanel(vizPanel, isSnapshot),
};
let panel: Panel;
if (isLibraryPanel(vizPanel)) {
const libPanel = getLibraryPanelBehavior(vizPanel);
panel = {
id: getPanelIdForVizPanel(vizPanel),
title: libPanel!.state.title,
gridPos: gridPos,
libraryPanel: {
name: libPanel!.state.name,
uid: libPanel!.state.uid,
},
} as Panel;
return panel;
} else {
panel = {
id: getPanelIdForVizPanel(vizPanel),
type: vizPanel.state.pluginId,
title: vizPanel.state.title,
description: vizPanel.state.description ?? undefined,
gridPos,
fieldConfig: (vizPanel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] },
transformations: [],
transparent: vizPanel.state.displayMode === 'transparent',
pluginVersion: vizPanel.state.pluginVersion,
...vizPanelDataToPanel(vizPanel, isSnapshot),
};
}
if (vizPanel.state.options) {
const { angularOptions, ...rest } = vizPanel.state.options as any;
@ -342,9 +333,10 @@ export function panelRepeaterToPanels(
if (!isSnapshot) {
return [gridItemToPanel(repeater, sceneState)];
} else {
if (repeater.state.body instanceof LibraryVizPanel) {
// return early if the repeated panel is a library panel
if (repeater.state.body instanceof VizPanel && isLibraryPanel(repeater.state.body)) {
const { x = 0, y = 0, width: w = 0, height: h = 0 } = repeater.state;
return [libraryVizPanelToPanel(repeater.state.body, { x, y, w, h })];
return [vizPanelToPanel(repeater.state.body, { x, y, w, h }, isSnapshot)];
}
if (repeater.state.repeatedPanels) {

View File

@ -8,9 +8,8 @@ import { t } from 'app/core/internationalization';
import { isPublicDashboardsEnabled } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
import { getTrackingSource } from '../../dashboard/components/ShareModal/utils';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { DashboardInteractions } from '../utils/interactions';
import { getDashboardSceneFor } from '../utils/utils';
import { getDashboardSceneFor, isLibraryPanel } from '../utils/utils';
import { ShareExportTab } from './ShareExportTab';
import { ShareLibraryPanelTab } from './ShareLibraryPanelTab';
@ -66,9 +65,8 @@ export class ShareModal extends SceneObjectBase<ShareModalState> implements Moda
if (panelRef) {
tabs.push(new SharePanelEmbedTab({ panelRef }));
const panel = panelRef.resolve();
const isLibraryPanel = panel.parent instanceof LibraryVizPanel;
if (panel instanceof VizPanel) {
if (!isLibraryPanel) {
if (!isLibraryPanel(panel)) {
tabs.push(new ShareLibraryPanelTab({ panelRef, modalRef }));
}
}

View File

@ -12,7 +12,6 @@ import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { DashboardGridItem } from '../../scene/DashboardGridItem';
import { DashboardScene } from '../../scene/DashboardScene';
import { LibraryVizPanel } from '../../scene/LibraryVizPanel';
export const getUnsupportedDashboardDatasources = async (types: string[]): Promise<string[]> => {
let unsupportedDS = new Set<string>();
@ -64,12 +63,10 @@ function rowTypes(gridRow: SceneGridRow) {
}
function panelDatasourceTypes(gridItem: SceneGridItemLike) {
let vizPanel: VizPanel | LibraryVizPanel | undefined;
let vizPanel: VizPanel | undefined;
if (gridItem instanceof DashboardGridItem) {
if (gridItem.state.body instanceof LibraryVizPanel) {
vizPanel = gridItem.state.body.state.panel;
} else if (gridItem.state.body instanceof VizPanel) {
if (gridItem.state.body instanceof VizPanel) {
vizPanel = gridItem.state.body;
} else {
throw new Error('DashboardGridItem body expected to be VizPanel');

View File

@ -1,6 +1,6 @@
import { VizPanel } from '@grafana/scenes';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { PanelModelCompatibilityWrapper } from './PanelModelCompatibilityWrapper';
@ -12,15 +12,19 @@ describe('PanelModelCompatibilityWrapper', () => {
});
it('Can get legacy id for lib panel', () => {
const libPanel = new LibraryVizPanel({
const panel = new VizPanel({ pluginId: 'test', title: 'test', description: 'test', key: 'panel-24' });
const libPanel = new LibraryPanelBehavior({
uid: 'a',
name: 'aa',
title: 'a',
panelKey: 'panel-24',
panel: new VizPanel({ pluginId: 'test', title: 'test', description: 'test', key: 'panel-24' }),
});
const panelModel = new PanelModelCompatibilityWrapper(libPanel.state.panel!);
panel.setState({
$behaviors: [libPanel],
});
const panelModel = new PanelModelCompatibilityWrapper(panel);
expect(panelModel.id).toBe(24);
});
});

View File

@ -6,7 +6,7 @@ import { DashboardControls } from '../scene/DashboardControls';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { dashboardSceneGraph, getNextPanelId } from './dashboardSceneGraph';
@ -129,11 +129,16 @@ describe('dashboardSceneGraph', () => {
}),
}),
new DashboardGridItem({
body: new LibraryVizPanel({
uid: 'uid',
name: 'LibPanel',
title: 'Library Panel',
panelKey: 'panel-2',
body: new VizPanel({
title: 'Library Panel 1',
key: 'panel-2',
$behaviors: [
new LibraryPanelBehavior({
uid: 'uid',
name: 'LibPanel',
title: 'Library Panel 1',
}),
],
}),
}),
new DashboardGridItem({
@ -166,11 +171,16 @@ describe('dashboardSceneGraph', () => {
}),
}),
new DashboardGridItem({
body: new LibraryVizPanel({
uid: 'uid',
name: 'LibPanel',
title: 'Library Panel',
panelKey: 'panel-3',
body: new VizPanel({
title: 'Library Panel 2',
key: 'panel-3',
$behaviors: [
new LibraryPanelBehavior({
uid: 'uid',
name: 'LibPanel',
title: 'Library Panel 2',
}),
],
}),
}),
],

View File

@ -3,10 +3,9 @@ import { VizPanel, SceneGridRow, sceneGraph, SceneGridLayout, behaviors } from '
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene } from '../scene/DashboardScene';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { VizPanelLinks } from '../scene/PanelLinks';
import { getPanelIdForLibraryVizPanel, getPanelIdForVizPanel } from './utils';
import { getPanelIdForVizPanel } from './utils';
function getTimePicker(scene: DashboardScene) {
return scene.state.controls?.state.timePicker;
@ -87,10 +86,7 @@ export function getNextPanelId(dashboard: DashboardScene): number {
const vizPanel = child.state.body;
if (vizPanel) {
const panelId =
vizPanel instanceof LibraryVizPanel
? getPanelIdForLibraryVizPanel(vizPanel)
: getPanelIdForVizPanel(vizPanel);
const panelId = getPanelIdForVizPanel(vizPanel);
if (panelId > max) {
max = panelId;
@ -111,10 +107,7 @@ export function getNextPanelId(dashboard: DashboardScene): number {
const vizPanel = rowChild.state.body;
if (vizPanel) {
const panelId =
vizPanel instanceof LibraryVizPanel
? getPanelIdForLibraryVizPanel(vizPanel)
: getPanelIdForVizPanel(vizPanel);
const panelId = getPanelIdForVizPanel(vizPanel);
if (panelId > max) {
max = panelId;
@ -128,19 +121,6 @@ export function getNextPanelId(dashboard: DashboardScene): number {
return max + 1;
}
// Returns the LibraryVizPanel that corresponds to the given VizPanel if it exists
export const getLibraryVizPanelFromVizPanel = (vizPanel: VizPanel): LibraryVizPanel | null => {
if (vizPanel.parent instanceof LibraryVizPanel) {
return vizPanel.parent;
}
if (vizPanel.parent instanceof DashboardGridItem && vizPanel.parent.state.body instanceof LibraryVizPanel) {
return vizPanel.parent.state.body;
}
return null;
};
export const dashboardSceneGraph = {
getTimePicker,
getRefreshPicker,

View File

@ -16,7 +16,6 @@ import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/co
import { DashboardDTO } from 'app/types';
import { DashboardGridItem, RepeatDirection } from '../scene/DashboardGridItem';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
@ -107,7 +106,7 @@ interface SceneOptions {
variableRefresh?: VariableRefresh;
}
export function buildPanelRepeaterScene(options: SceneOptions, source?: VizPanel | LibraryVizPanel) {
export function buildPanelRepeaterScene(options: SceneOptions, source?: VizPanel) {
const defaults = { usePanelRepeater: true, ...options };
const withRepeat = new DashboardGridItem({

View File

@ -15,7 +15,7 @@ import { initialIntervalVariableModelState } from 'app/features/variables/interv
import { DashboardDatasourceBehaviour } from '../scene/DashboardDatasourceBehaviour';
import { DashboardScene } from '../scene/DashboardScene';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { panelMenuBehavior } from '../scene/PanelMenuBehavior';
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
@ -34,10 +34,6 @@ export function getPanelIdForVizPanel(panel: SceneObject): number {
return parseInt(panel.state.key!.replace('panel-', ''), 10);
}
export function getPanelIdForLibraryVizPanel(panel: LibraryVizPanel): number {
return parseInt(panel.state.panelKey!.replace('panel-', ''), 10);
}
/**
* This will also try lookup based on panelId
*/
@ -256,11 +252,18 @@ export function getDefaultRow(dashboard: DashboardScene): SceneGridRow {
});
}
export function getLibraryPanel(vizPanel: VizPanel): LibraryVizPanel | undefined {
if (vizPanel.parent instanceof LibraryVizPanel) {
return vizPanel.parent;
export function isLibraryPanel(vizPanel: VizPanel): boolean {
return getLibraryPanelBehavior(vizPanel) !== undefined;
}
export function getLibraryPanelBehavior(vizPanel: VizPanel): LibraryPanelBehavior | undefined {
const behavior = vizPanel.state.$behaviors?.find((behaviour) => behaviour instanceof LibraryPanelBehavior);
if (behavior) {
return behavior;
}
return;
return undefined;
}
/**

View File

@ -17,7 +17,7 @@ import {
import { VizPanel } from '@grafana/scenes';
import { Input } from '@grafana/ui';
import { LibraryVizPanelInfo } from 'app/features/dashboard-scene/panel-edit/LibraryVizPanelInfo';
import { LibraryVizPanel } from 'app/features/dashboard-scene/scene/LibraryVizPanel';
import { LibraryPanelBehavior } from 'app/features/dashboard-scene/scene/LibraryPanelBehavior';
import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor';
@ -150,7 +150,7 @@ export function getVisualizationOptions(props: OptionPaneRenderProps): OptionsPa
return Object.values(categoryIndex);
}
export function getLibraryVizPanelOptionsCategory(libraryPanel: LibraryVizPanel): OptionsPaneCategoryDescriptor {
export function getLibraryVizPanelOptionsCategory(libraryPanel: LibraryPanelBehavior): OptionsPaneCategoryDescriptor {
const descriptor = new OptionsPaneCategoryDescriptor({
title: 'Library panel options',
id: 'Library panel options',

View File

@ -1,10 +1,12 @@
import { lastValueFrom } from 'rxjs';
import { defaultDashboard } from '@grafana/schema';
import { VizPanel } from '@grafana/scenes';
import { LibraryPanel, defaultDashboard } from '@grafana/schema';
import { DashboardModel } from 'app/features/dashboard/state';
import { VizPanelManager } from 'app/features/dashboard-scene/panel-edit/VizPanelManager';
import { DashboardGridItem } from 'app/features/dashboard-scene/scene/DashboardGridItem';
import { LibraryVizPanel } from 'app/features/dashboard-scene/scene/LibraryVizPanel';
import { vizPanelToPanel } from 'app/features/dashboard-scene/serialization/transformSceneToSaveModel';
import { getLibraryPanelBehavior } from 'app/features/dashboard-scene/utils/utils';
import { getBackendSrv } from '../../../core/services/backend_srv';
import { DashboardSearchItem } from '../../search/types';
@ -137,22 +139,30 @@ export async function getConnectedDashboards(uid: string): Promise<DashboardSear
return searchHits;
}
export function libraryVizPanelToSaveModel(libraryPanel: LibraryVizPanel) {
const { panel, uid, name, _loadedPanel } = libraryPanel.state;
export function libraryVizPanelToSaveModel(vizPanel: VizPanel) {
const libraryPanelBehavior = getLibraryPanelBehavior(vizPanel);
const gridItem = libraryPanel.parent;
const { uid, name, _loadedPanel } = libraryPanelBehavior!.state;
if (!gridItem || !(gridItem instanceof DashboardGridItem)) {
throw new Error('LibraryVizPanel must be a child of DashboardGridItem');
let gridItem = vizPanel.parent;
if (gridItem instanceof VizPanelManager) {
gridItem = gridItem.state.sourcePanel.resolve().parent;
}
if (!gridItem || !(gridItem instanceof DashboardGridItem)) {
throw new Error('Trying to save a library panel that does not have a DashboardGridItem parent');
}
// we need all the panel properties to save the library panel,
// so we clone it and remove the behaviour to get what we need
const saveModel = {
..._loadedPanel,
uid,
folderUID: _loadedPanel?.folderUid,
name,
version: _loadedPanel?.version || 0,
type: vizPanel.state.pluginId,
model: vizPanelToPanel(
panel!,
vizPanel.clone({ $behaviors: undefined }),
{
x: gridItem.state.x ?? 0,
y: gridItem.state.y ?? 0,
@ -163,15 +173,16 @@ export function libraryVizPanelToSaveModel(libraryPanel: LibraryVizPanel) {
gridItem
),
kind: LibraryElementKind.Panel,
version: _loadedPanel?.version || 0,
};
return saveModel;
}
export async function updateLibraryVizPanel(libraryPanel: LibraryVizPanel): Promise<LibraryElementDTO> {
const { uid, folderUID, name, model, version, kind } = libraryVizPanelToSaveModel(libraryPanel);
export async function updateLibraryVizPanel(vizPanel: VizPanel): Promise<LibraryPanel> {
const { uid, folderUid, name, model, version, kind } = libraryVizPanelToSaveModel(vizPanel);
const { result } = await getBackendSrv().patch(`/api/library-elements/${uid}`, {
folderUID,
folderUid,
name,
model,
version,
@ -179,3 +190,15 @@ export async function updateLibraryVizPanel(libraryPanel: LibraryVizPanel): Prom
});
return result;
}
export async function saveLibPanel(panel: VizPanel) {
const updatedLibPanel = await updateLibraryVizPanel(panel);
const libPanelBehavior = getLibraryPanelBehavior(panel);
if (!libPanelBehavior) {
throw new Error('Could not find library panel behavior for panel');
}
libPanelBehavior.setPanelFromLibPanel(updatedLibPanel);
}

View File

@ -1144,6 +1144,7 @@
"empty-state": {
"message": "No library panels found"
},
"loading-panel-text": "Loading library panel",
"modal": {
"body_one": "This panel is being used in {{count}} dashboard. Please choose which dashboard to view the panel in:",
"body_other": "This panel is being used in {{count}} dashboard. Please choose which dashboard to view the panel in:",

View File

@ -1144,6 +1144,7 @@
"empty-state": {
"message": "Ńő ľįþřäřy päʼnęľş ƒőūʼnđ"
},
"loading-panel-text": "Ŀőäđįʼnģ ľįþřäřy päʼnęľ",
"modal": {
"body_one": "Ŧĥįş päʼnęľ įş þęįʼnģ ūşęđ įʼn {{count}} đäşĥþőäřđ. Pľęäşę čĥőőşę ŵĥįčĥ đäşĥþőäřđ ŧő vįęŵ ŧĥę päʼnęľ įʼn:",
"body_other": "Ŧĥįş päʼnęľ įş þęįʼnģ ūşęđ įʼn {{count}} đäşĥþőäřđ. Pľęäşę čĥőőşę ŵĥįčĥ đäşĥþőäřđ ŧő vįęŵ ŧĥę päʼnęľ įʼn:",