Dashboard scenes: Unlink library panel inside edit mode (#84355)

Unlink library panel inside edit mode
This commit is contained in:
Oscar Kilhed 2024-03-13 18:22:22 +01:00 committed by GitHub
parent cf6bed7ae5
commit 0fe5b62fa5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 119 additions and 8 deletions

View File

@ -20,6 +20,7 @@ export interface PanelEditorState extends SceneObjectState {
dataPane?: PanelDataPane; dataPane?: PanelDataPane;
vizManager: VizPanelManager; vizManager: VizPanelManager;
showLibraryPanelSaveModal?: boolean; showLibraryPanelSaveModal?: boolean;
showLibraryPanelUnlinkModal?: boolean;
} }
export class PanelEditor extends SceneObjectBase<PanelEditorState> { export class PanelEditor extends SceneObjectBase<PanelEditorState> {
@ -212,9 +213,22 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
locationService.partial({ editPanel: null }); locationService.partial({ editPanel: null });
}; };
public onDismissLibraryPanelModal = () => { public onDismissLibraryPanelSaveModal = () => {
this.setState({ showLibraryPanelSaveModal: false }); 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 { export function buildPanelEditScene(panel: VizPanel): PanelEditor {

View File

@ -6,6 +6,7 @@ import { SceneComponentProps } from '@grafana/scenes';
import { Button, ToolbarButton, useStyles2 } from '@grafana/ui'; import { Button, ToolbarButton, useStyles2 } from '@grafana/ui';
import { NavToolbarActions } from '../scene/NavToolbarActions'; import { NavToolbarActions } from '../scene/NavToolbarActions';
import { UnlinkModal } from '../scene/UnlinkModal';
import { getDashboardSceneFor, getLibraryPanel } from '../utils/utils'; import { getDashboardSceneFor, getLibraryPanel } from '../utils/utils';
import { PanelEditor } from './PanelEditor'; import { PanelEditor } from './PanelEditor';
@ -58,7 +59,7 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
function VizAndDataPane({ model }: SceneComponentProps<PanelEditor>) { function VizAndDataPane({ model }: SceneComponentProps<PanelEditor>) {
const dashboard = getDashboardSceneFor(model); const dashboard = getDashboardSceneFor(model);
const { vizManager, dataPane, showLibraryPanelSaveModal } = model.useState(); const { vizManager, dataPane, showLibraryPanelSaveModal, showLibraryPanelUnlinkModal } = model.useState();
const { sourcePanel } = vizManager.useState(); const { sourcePanel } = vizManager.useState();
const libraryPanel = getLibraryPanel(sourcePanel.resolve()); const libraryPanel = getLibraryPanel(sourcePanel.resolve());
const { controls } = dashboard.useState(); const { controls } = dashboard.useState();
@ -88,11 +89,18 @@ function VizAndDataPane({ model }: SceneComponentProps<PanelEditor>) {
{showLibraryPanelSaveModal && libraryPanel && ( {showLibraryPanelSaveModal && libraryPanel && (
<SaveLibraryVizPanelModal <SaveLibraryVizPanelModal
libraryPanel={libraryPanel} libraryPanel={libraryPanel}
onDismiss={model.onDismissLibraryPanelModal} onDismiss={model.onDismissLibraryPanelSaveModal}
onConfirm={model.onConfirmSaveLibraryPanel} onConfirm={model.onConfirmSaveLibraryPanel}
onDiscard={model.onDiscard} onDiscard={model.onDiscard}
></SaveLibraryVizPanelModal> ></SaveLibraryVizPanelModal>
)} )}
{showLibraryPanelUnlinkModal && libraryPanel && (
<UnlinkModal
onDismiss={model.onDismissUnlinkLibraryPanelModal}
onConfirm={model.onConfirmUnlinkLibraryPanel}
isOpen
/>
)}
{dataPane && ( {dataPane && (
<> <>
<div {...splitterProps} /> <div {...splitterProps} />

View File

@ -250,6 +250,40 @@ describe('VizPanelManager', () => {
expect(apiCall.mock.calls[0][0].state.panel?.state.title).toBe('new title'); 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', () => { describe('query options', () => {

View File

@ -338,6 +338,24 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
}); });
} }
public unlinkLibraryPanel() {
const sourcePanel = this.state.sourcePanel.resolve();
if (!(sourcePanel.parent instanceof LibraryVizPanel)) {
throw new Error('VizPanel is not a child of a library panel');
}
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() { public commitChanges() {
const sourcePanel = this.state.sourcePanel.resolve(); const sourcePanel = this.state.sourcePanel.resolve();

View File

@ -1,5 +1,5 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React from 'react'; import React, { useEffect, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; 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 { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator'; import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator';
import { contextSrv } from 'app/core/core'; 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 { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
import { PanelEditor } from '../panel-edit/PanelEditor';
import { ShareModal } from '../sharing/ShareModal'; import { ShareModal } from '../sharing/ShareModal';
import { DashboardInteractions } from '../utils/interactions'; import { DashboardInteractions } from '../utils/interactions';
import { dynamicDashNavActions } from '../utils/registerDynamicDashNavAction'; import { dynamicDashNavActions } from '../utils/registerDynamicDashNavAction';
@ -57,9 +58,9 @@ export function ToolbarActions({ dashboard }: Props) {
const buttonWithExtraMargin = useStyles2(getStyles); const buttonWithExtraMargin = useStyles2(getStyles);
const isEditingPanel = Boolean(editPanel); const isEditingPanel = Boolean(editPanel);
const isViewingPanel = Boolean(viewPanelScene); const isViewingPanel = Boolean(viewPanelScene);
const isEditingLibraryPanel = Boolean(
editPanel?.state.vizManager.state.sourcePanel.resolve().parent instanceof LibraryVizPanel const isEditingLibraryPanel = useEditingLibraryPanel(editPanel);
);
const hasCopiedPanel = Boolean(copiedPanel); const hasCopiedPanel = Boolean(copiedPanel);
toolbarActions.push({ toolbarActions.push({
@ -383,6 +384,23 @@ export function ToolbarActions({ dashboard }: Props) {
), ),
}); });
toolbarActions.push({
group: 'main-buttons',
condition: isEditingPanel && isEditingLibraryPanel && !editview && !isViewingPanel,
render: () => (
<Button
onClick={editPanel?.onUnlinkLibraryPanel}
tooltip="Unlink library panel"
size="sm"
key="unlinkLibraryPanel"
fill="outline"
variant="secondary"
>
Unlink library panel
</Button>
),
});
toolbarActions.push({ toolbarActions.push({
group: 'main-buttons', group: 'main-buttons',
condition: isEditingPanel && isEditingLibraryPanel && !editview && !isViewingPanel, condition: isEditingPanel && isEditingLibraryPanel && !editview && !isViewingPanel,
@ -504,6 +522,25 @@ export function ToolbarActions({ dashboard }: Props) {
return actionElements; return actionElements;
} }
function useEditingLibraryPanel(panelEditor?: PanelEditor) {
const [isEditingLibraryPanel, setEditingLibraryPanel] = useState<Boolean>(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 { interface ToolbarAction {
group: string; group: string;
condition?: boolean | string; condition?: boolean | string;