diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index 7d2a1ceee2e..cb1a6eb4d8d 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -20,6 +20,7 @@ export interface PanelEditorState extends SceneObjectState { dataPane?: PanelDataPane; vizManager: VizPanelManager; showLibraryPanelSaveModal?: boolean; + showLibraryPanelUnlinkModal?: boolean; } export class PanelEditor extends SceneObjectBase { @@ -212,9 +213,22 @@ export class PanelEditor extends SceneObjectBase { locationService.partial({ editPanel: null }); }; - public onDismissLibraryPanelModal = () => { + public onDismissLibraryPanelSaveModal = () => { this.setState({ showLibraryPanelSaveModal: false }); }; + + public onUnlinkLibraryPanel = () => { + this.setState({ showLibraryPanelUnlinkModal: true }); + }; + + public onDismissUnlinkLibraryPanelModal = () => { + this.setState({ showLibraryPanelUnlinkModal: false }); + }; + + public onConfirmUnlinkLibraryPanel = () => { + this.state.vizManager.unlinkLibraryPanel(); + this.setState({ showLibraryPanelUnlinkModal: false }); + }; } export function buildPanelEditScene(panel: VizPanel): PanelEditor { diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx index 38270d798f8..73da63e67ae 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx @@ -6,6 +6,7 @@ import { SceneComponentProps } from '@grafana/scenes'; import { Button, ToolbarButton, useStyles2 } from '@grafana/ui'; import { NavToolbarActions } from '../scene/NavToolbarActions'; +import { UnlinkModal } from '../scene/UnlinkModal'; import { getDashboardSceneFor, getLibraryPanel } from '../utils/utils'; import { PanelEditor } from './PanelEditor'; @@ -58,7 +59,7 @@ export function PanelEditorRenderer({ model }: SceneComponentProps) function VizAndDataPane({ model }: SceneComponentProps) { const dashboard = getDashboardSceneFor(model); - const { vizManager, dataPane, showLibraryPanelSaveModal } = model.useState(); + const { vizManager, dataPane, showLibraryPanelSaveModal, showLibraryPanelUnlinkModal } = model.useState(); const { sourcePanel } = vizManager.useState(); const libraryPanel = getLibraryPanel(sourcePanel.resolve()); const { controls } = dashboard.useState(); @@ -88,11 +89,18 @@ function VizAndDataPane({ model }: SceneComponentProps) { {showLibraryPanelSaveModal && libraryPanel && ( )} + {showLibraryPanelUnlinkModal && libraryPanel && ( + + )} {dataPane && ( <>
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 3e13b91c3bc..98f4a7143c7 100644 --- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx @@ -250,6 +250,40 @@ describe('VizPanelManager', () => { expect(apiCall.mock.calls[0][0].state.panel?.state.title).toBe('new title'); }); + + it('unlinks library panel', () => { + const panel = new VizPanel({ + key: 'panel-1', + pluginId: 'text', + }); + + const libraryPanelModel = { + title: 'title', + uid: 'uid', + name: 'libraryPanelName', + model: vizPanelToPanel(panel), + type: 'panel', + version: 1, + }; + + const libraryPanel = new LibraryVizPanel({ + isLoaded: true, + title: libraryPanelModel.title, + uid: libraryPanelModel.uid, + name: libraryPanelModel.name, + panelKey: panel.state.key!, + panel: panel, + _loadedPanel: libraryPanelModel, + }); + + const gridItem = new SceneGridItem({ body: libraryPanel }); + + const panelManager = VizPanelManager.createFor(panel); + panelManager.unlinkLibraryPanel(); + + const sourcePanel = panelManager.state.sourcePanel.resolve(); + expect(sourcePanel.parent?.state.key).toBe(gridItem.state.key); + }); }); describe('query options', () => { diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx index 5f8c78e5b6a..94d9e62fce0 100644 --- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx +++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx @@ -338,6 +338,24 @@ 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'); + } + + const gridItem = sourcePanel.parent.parent; + if (!(gridItem instanceof SceneGridItem)) { + throw new Error('Library panel not a child of a grid item'); + } + + const newSourcePanel = this.state.panel.clone({ $data: this.state.$data?.clone() }); + gridItem.setState({ + body: newSourcePanel, + }); + this.setState({ sourcePanel: newSourcePanel.getRef() }); + } + public commitChanges() { const sourcePanel = this.state.sourcePanel.resolve(); diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 9bfd56f1555..ab171dcd870 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -8,10 +8,11 @@ import { Button, ButtonGroup, Dropdown, Icon, Menu, ToolbarButton, ToolbarButton import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator'; import { contextSrv } from 'app/core/core'; -import { t, Trans } from 'app/core/internationalization'; +import { Trans, t } from 'app/core/internationalization'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; +import { PanelEditor } from '../panel-edit/PanelEditor'; import { ShareModal } from '../sharing/ShareModal'; import { DashboardInteractions } from '../utils/interactions'; import { dynamicDashNavActions } from '../utils/registerDynamicDashNavAction'; @@ -57,9 +58,9 @@ export function ToolbarActions({ dashboard }: Props) { const buttonWithExtraMargin = useStyles2(getStyles); const isEditingPanel = Boolean(editPanel); const isViewingPanel = Boolean(viewPanelScene); - const isEditingLibraryPanel = Boolean( - editPanel?.state.vizManager.state.sourcePanel.resolve().parent instanceof LibraryVizPanel - ); + + const isEditingLibraryPanel = useEditingLibraryPanel(editPanel); + const hasCopiedPanel = Boolean(copiedPanel); toolbarActions.push({ @@ -383,6 +384,23 @@ export function ToolbarActions({ dashboard }: Props) { ), }); + toolbarActions.push({ + group: 'main-buttons', + condition: isEditingPanel && isEditingLibraryPanel && !editview && !isViewingPanel, + render: () => ( + + ), + }); + toolbarActions.push({ group: 'main-buttons', condition: isEditingPanel && isEditingLibraryPanel && !editview && !isViewingPanel, @@ -504,6 +522,25 @@ export function ToolbarActions({ dashboard }: Props) { return actionElements; } +function useEditingLibraryPanel(panelEditor?: PanelEditor) { + const [isEditingLibraryPanel, setEditingLibraryPanel] = useState(false); + + useEffect(() => { + if (panelEditor) { + const unsub = panelEditor.state.vizManager.subscribeToState((vizManagerState) => + setEditingLibraryPanel(vizManagerState.sourcePanel.resolve().parent instanceof LibraryVizPanel) + ); + return () => { + unsub.unsubscribe(); + }; + } + setEditingLibraryPanel(false); + return; + }, [panelEditor]); + + return isEditingLibraryPanel; +} + interface ToolbarAction { group: string; condition?: boolean | string;