diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index bfad9110786..69b1e38091d 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -490,6 +490,39 @@ describe('DashboardScene', () => { const gridRow = body.state.children[2] as SceneGridRow; expect(gridRow.state.children.length).toBe(1); }); + + 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 scene = buildTestScene({ + body: new SceneGridLayout({ + children: [ + new SceneGridItem({ + key: 'griditem-2', + body: libPanel, + }), + ], + }), + }); + + scene.unlinkLibraryPanel(libPanel); + + const body = scene.state.body as SceneGridLayout; + const gridItem = body.state.children[0] as SceneGridItem; + + expect(body.state.children.length).toBe(1); + expect(gridItem.state.body).toBeInstanceOf(VizPanel); + }); }); }); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index a08933bac67..be27cb411a5 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -603,6 +603,23 @@ export class DashboardScene extends SceneObjectBase { } } + public unlinkLibraryPanel(panel: LibraryVizPanel) { + if (!panel.parent) { + return; + } + + const gridItem = panel.parent; + + if (!(gridItem instanceof SceneGridItem || gridItem instanceof PanelRepeaterGridItem)) { + console.error('Trying to duplicate a panel in a layout that is not SceneGridItem or PanelRepeaterGridItem'); + return; + } + + gridItem?.setState({ + body: panel.state.panel?.clone(), + }); + } + public showModal(modal: SceneObject) { this.setState({ overlay: modal }); } diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index 84d7870b5ef..323c89e7608 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -28,6 +28,7 @@ import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from ' import { DashboardScene } from './DashboardScene'; import { LibraryVizPanel } from './LibraryVizPanel'; import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks'; +import { UnlinkLibraryPanelModal } from './UnlinkLibraryPanelModal'; /** * Behavior is called when VizPanelMenu is activated (ie when it's opened). @@ -37,6 +38,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) { // 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[] = []; @@ -101,8 +103,18 @@ export function panelMenuBehavior(menu: VizPanelMenu) { }, }); - if (panel.parent instanceof LibraryVizPanel) { - // TODO: Implement lib panel unlinking + if (parent instanceof LibraryVizPanel) { + moreSubMenu.push({ + text: t('panel.header-menu.unlink-library-panel', `Unlink library panel`), + onClick: () => { + DashboardInteractions.panelMenuItemClicked('unlinkLibraryPanel'); + dashboard.showModal( + new UnlinkLibraryPanelModal({ + panelRef: parent.getRef(), + }) + ); + }, + }); } else { moreSubMenu.push({ text: t('panel.header-menu.create-library-panel', `Create library panel`), diff --git a/public/app/features/dashboard-scene/scene/UnlinkLibraryPanelModal.tsx b/public/app/features/dashboard-scene/scene/UnlinkLibraryPanelModal.tsx new file mode 100644 index 00000000000..8dc2696a71d --- /dev/null +++ b/public/app/features/dashboard-scene/scene/UnlinkLibraryPanelModal.tsx @@ -0,0 +1,44 @@ +import React from 'react'; + +import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState } 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; +} + +export class UnlinkLibraryPanelModal + extends SceneObjectBase + implements ModalSceneObjectLike +{ + static Component = UnlinkLibraryPanelModalRenderer; + + public onDismiss = () => { + const dashboard = getDashboardSceneFor(this); + dashboard.closeModal(); + }; + + public onConfirm = () => { + const dashboard = getDashboardSceneFor(this); + dashboard.unlinkLibraryPanel(this.state.panelRef!.resolve()); + dashboard.closeModal(); + }; +} + +function UnlinkLibraryPanelModalRenderer({ model }: SceneComponentProps) { + return ( + { + model.onConfirm(); + model.onDismiss(); + }} + onDismiss={model.onDismiss} + /> + ); +} diff --git a/public/app/features/library-panels/components/UnlinkModal/UnlinkModal.tsx b/public/app/features/dashboard-scene/scene/UnlinkModal.tsx similarity index 100% rename from public/app/features/library-panels/components/UnlinkModal/UnlinkModal.tsx rename to public/app/features/dashboard-scene/scene/UnlinkModal.tsx diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index eb31dae7c04..1577591e4bc 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -33,7 +33,7 @@ import { StoreState } from 'app/types'; import { PanelOptionsChangedEvent, ShowModalReactEvent } from 'app/types/events'; import { notifyApp } from '../../../../core/actions'; -import { UnlinkModal } from '../../../library-panels/components/UnlinkModal/UnlinkModal'; +import { UnlinkModal } from '../../../dashboard-scene/scene/UnlinkModal'; import { isPanelModelLibraryPanel } from '../../../library-panels/guard'; import { getVariablesByKey } from '../../../variables/state/selectors'; import { DashboardPanel } from '../../dashgrid/DashboardPanel'; diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index cd178de9a4d..bc12af97840 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -9,8 +9,8 @@ import store from 'app/core/store'; import { ShareModal } from 'app/features/dashboard/components/ShareModal'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; +import { UnlinkModal } from 'app/features/dashboard-scene/scene/UnlinkModal'; import { AddLibraryPanelModal } from 'app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal'; -import { UnlinkModal } from 'app/features/library-panels/components/UnlinkModal/UnlinkModal'; import { cleanUpPanelState } from 'app/features/panel/state/actions'; import { dispatch } from 'app/store/store';