diff --git a/public/app/features/dashboard-scene/inspect/HelpWizard/utils.ts b/public/app/features/dashboard-scene/inspect/HelpWizard/utils.ts index b4ad0c41b37..d3cab702aba 100644 --- a/public/app/features/dashboard-scene/inspect/HelpWizard/utils.ts +++ b/public/app/features/dashboard-scene/inspect/HelpWizard/utils.ts @@ -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 = { 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), diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx index a357890c034..84e7df9aa7c 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx @@ -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(), }); diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx index 8091daf5d8a..c1424bfeeb3 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx @@ -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 { 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) { diff --git a/public/app/features/dashboard-scene/panel-edit/LibraryVizPanelInfo.tsx b/public/app/features/dashboard-scene/panel-edit/LibraryVizPanelInfo.tsx index 3e850af79b6..dcddc75965f 100644 --- a/public/app/features/dashboard-scene/panel-edit/LibraryVizPanelInfo.tsx +++ b/public/app/features/dashboard-scene/panel-edit/LibraryVizPanelInfo.tsx @@ -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) => { diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts index abca0835fc4..1c18e13fa0d 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts @@ -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'); }); }); diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index 616acfc8238..40546061ae5 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -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 { } public onDiscard = () => { + this.state.vizManager.setState({ isDirty: false }); this._discardChanges = true; locationService.partial({ editPanel: null }); }; @@ -106,15 +106,7 @@ export class PanelEditor extends SceneObjectBase { 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 { public onConfirmSaveLibraryPanel = () => { this.state.vizManager.commitChanges(); + this.state.vizManager.setState({ isDirty: false }); locationService.partial({ editPanel: null }); }; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx index b40100ab903..1ac75e7e274 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx @@ -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) { 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); diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx index fbecf91ece8..619183a9dd2 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx @@ -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' ); }); diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx index 3778f882a2c..d342f9e3ad3 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx @@ -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(({ 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(({ 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( () => diff --git a/public/app/features/dashboard-scene/panel-edit/SaveLibraryVizPanelModal.tsx b/public/app/features/dashboard-scene/panel-edit/SaveLibraryVizPanelModal.tsx index c9538e7bdc7..6fc675762e3 100644 --- a/public/app/features/dashboard-scene/panel-edit/SaveLibraryVizPanelModal.tsx +++ b/public/app/features/dashboard-scene/panel-edit/SaveLibraryVizPanelModal.tsx @@ -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; diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx index 7c79f5798cc..887c876bf70 100644 --- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx @@ -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); }); }); diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx index 6878fd5e569..c27ed98146c 100644 --- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx +++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx @@ -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 { public static createFor(sourcePanel: VizPanel) { let repeatOptions: Pick = {}; - 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 { 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 { }); } - this.setState({ - pluginId, - }); - + this.setState({ pluginId }); this.state.panel.changePluginType(pluginId, cachedOptions, newFieldConfig); this.loadDataSource(); @@ -405,21 +407,22 @@ export class VizPanelManager extends SceneObjectBase { 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 { 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 { */ 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' }; diff --git a/public/app/features/dashboard-scene/saving/DashboardPrompt.tsx b/public/app/features/dashboard-scene/saving/DashboardPrompt.tsx index f7e737d3996..e841f7a47ba 100644 --- a/public/app/features/dashboard-scene/saving/DashboardPrompt.tsx +++ b/public/app/features/dashboard-scene/saving/DashboardPrompt.tsx @@ -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) { diff --git a/public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.ts b/public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.ts index bf30ddd9e99..dc31efab048 100644 --- a/public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.ts +++ b/public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.ts @@ -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; } diff --git a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx index 98cd9aa1993..90116dec207 100644 --- a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx +++ b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx @@ -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'); }); }); diff --git a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx index 30bc359ca6c..54f7679c87a 100644 --- a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx +++ b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.tsx @@ -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; + panelToReplaceRef?: SceneObjectRef; } export class AddLibraryPanelDrawer extends SceneObjectBase { @@ -38,11 +39,9 @@ export class AddLibraryPanelDrawer extends SceneObjectBase { }); 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); }); }); diff --git a/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.tsx b/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.tsx index d8746198e00..05880cecc19 100644 --- a/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.tsx @@ -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 { - 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(); - } - } } diff --git a/public/app/features/dashboard-scene/scene/DashboardGridItem.tsx b/public/app/features/dashboard-scene/scene/DashboardGridItem.tsx index cfdbaea407c..9e69fc477d9 100644 --- a/public/app/features/dashboard-scene/scene/DashboardGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardGridItem.tsx @@ -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 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 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 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 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 i if (body instanceof VizPanel) { return ; } - - if (body instanceof LibraryVizPanel) { - return ; - } - - if (body instanceof AddLibraryPanelDrawer) { - return ; - } } if (!repeatedPanels) { diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 10fb8788a03..a9b60e80ecd 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -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) { }), }), 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) { }), }), 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' })], }), }), ], diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 3378af54647..29eed253f20 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { } } - public unlinkLibraryPanel(panel: LibraryVizPanel) { + public unlinkLibraryPanel(panel: VizPanel) { if (!panel.parent) { return; } @@ -772,13 +719,11 @@ export class DashboardScene extends SceneObjectBase { 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 { locationService.partial({ editview: 'settings' }); }; - public onShowAddLibraryPanelDrawer(panelToReplaceRef?: SceneObjectRef) { + public onShowAddLibraryPanelDrawer(panelToReplaceRef?: SceneObjectRef) { this.setState({ overlay: new AddLibraryPanelDrawer({ panelToReplaceRef }), }); diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts index a169d05a60f..ec868f4bf7b 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts @@ -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 { diff --git a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx new file mode 100644 index 00000000000..7a558d8c5f5 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx @@ -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({ + 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 }; +} diff --git a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx new file mode 100644 index 00000000000..501f38e527f --- /dev/null +++ b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx @@ -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 { + 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 ( + + Loading library panel + + ); +} + +sceneUtils.registerRuntimePanelPlugin({ + pluginId: LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID, + plugin: LoadingVizPanelPlugin, +}); diff --git a/public/app/features/dashboard-scene/scene/LibraryVizPanel.test.ts b/public/app/features/dashboard-scene/scene/LibraryVizPanel.test.ts deleted file mode 100644 index 5bb05548356..00000000000 --- a/public/app/features/dashboard-scene/scene/LibraryVizPanel.test.ts +++ /dev/null @@ -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, 'model'> & { model?: Partial } = {} -) { - const libPanel: LibraryPanel = merge( - { - folderUid: 'general', - uid: 'fdcvggvfy2qdca', - name: 'My Library Panel', - type: 'timeseries', - description: '', - model: { - datasource: { - type: 'grafana-testdata-datasource', - uid: 'PD8C576611E62080A', - }, - description: '', - - maxPerRow: 4, - options: { - legend: { - calcs: [], - displayMode: 'list', - placement: 'bottom', - showLegend: true, - }, - tooltip: { - maxHeight: 600, - mode: 'single', - sort: 'none', - }, - }, - targets: [ - { - datasource: { - type: 'grafana-testdata-datasource', - uid: 'PD8C576611E62080A', - }, - refId: 'A', - }, - ], - title: 'Panel Title', - type: 'timeseries', - }, - version: 6, - meta: { - folderName: '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))); -} diff --git a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx b/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx deleted file mode 100644 index c5b3d172817..00000000000 --- a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx +++ /dev/null @@ -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 { - 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) { - const { panel } = model.useState(); - - if (!panel) { - return null; - } - - return ; -} diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 07ab8bfd828..d062d96a7f0 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -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(); diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index 9d3d657401b..a4442ac44ed 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -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 { diff --git a/public/app/features/dashboard-scene/scene/UnlinkLibraryPanelModal.tsx b/public/app/features/dashboard-scene/scene/UnlinkLibraryPanelModal.tsx index 3db5c17c0c1..3b1db237165 100644 --- a/public/app/features/dashboard-scene/scene/UnlinkLibraryPanelModal.tsx +++ b/public/app/features/dashboard-scene/scene/UnlinkLibraryPanelModal.tsx @@ -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; + panelRef?: SceneObjectRef; } export class UnlinkLibraryPanelModal diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index f1e4361f04b..e5c7ab36bca 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -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); }); }); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index f2c1ce703f3..5f761ad10ab 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -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], diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts index 042d2cd2ce7..94497918e64 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts @@ -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) { - if (panel.libraryPanel) { - return buildGridItemForLibPanel(new PanelModel(panel))!; - } - return buildGridItemForPanel(new PanelModel(panel)); } diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index 1077fcc0145..45db9cf1fdb 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -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) { diff --git a/public/app/features/dashboard-scene/sharing/ShareModal.tsx b/public/app/features/dashboard-scene/sharing/ShareModal.tsx index da4d8c75320..7e76957e41c 100644 --- a/public/app/features/dashboard-scene/sharing/ShareModal.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareModal.tsx @@ -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 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 })); } } diff --git a/public/app/features/dashboard-scene/sharing/public-dashboards/utils.ts b/public/app/features/dashboard-scene/sharing/public-dashboards/utils.ts index ba10cb5e976..37b49dc9d29 100644 --- a/public/app/features/dashboard-scene/sharing/public-dashboards/utils.ts +++ b/public/app/features/dashboard-scene/sharing/public-dashboards/utils.ts @@ -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 => { let unsupportedDS = new Set(); @@ -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'); diff --git a/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts b/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts index ed445f65cf3..e6e44c7a583 100644 --- a/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts +++ b/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts @@ -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); }); }); diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts index e9cd110226d..7e97d9530dd 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts @@ -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', + }), + ], }), }), ], diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts index 5c85b33a284..7f6198fd605 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts @@ -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, diff --git a/public/app/features/dashboard-scene/utils/test-utils.ts b/public/app/features/dashboard-scene/utils/test-utils.ts index 6bc2f591cfb..aa55c8a7eac 100644 --- a/public/app/features/dashboard-scene/utils/test-utils.ts +++ b/public/app/features/dashboard-scene/utils/test-utils.ts @@ -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({ diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 0da64d3f52a..f80a8ee67af 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -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; } /** diff --git a/public/app/features/dashboard/components/PanelEditor/getVisualizationOptions.tsx b/public/app/features/dashboard/components/PanelEditor/getVisualizationOptions.tsx index eb5cdf588b9..0a607ee3ff3 100644 --- a/public/app/features/dashboard/components/PanelEditor/getVisualizationOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/getVisualizationOptions.tsx @@ -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', diff --git a/public/app/features/library-panels/state/api.ts b/public/app/features/library-panels/state/api.ts index ce5faba1f2c..c92d295b3d6 100644 --- a/public/app/features/library-panels/state/api.ts +++ b/public/app/features/library-panels/state/api.ts @@ -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 { - const { uid, folderUID, name, model, version, kind } = libraryVizPanelToSaveModel(libraryPanel); +export async function updateLibraryVizPanel(vizPanel: VizPanel): Promise { + 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); +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index b4f006446c3..20bbe9e596d 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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:", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index e13ef14d739..e4a718a3cc9 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -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:",