mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DashboardScene: Panel edit toolbar actions (#82302)
* DashboardScene: Panel edit toolbar actions * Make saving work * Update public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com> --------- Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { PanelPlugin, PanelPluginMeta, PluginType } from '@grafana/data';
|
import { PanelPlugin, PanelPluginMeta, PluginType } from '@grafana/data';
|
||||||
import { SceneFlexItem, SplitLayout, VizPanel } from '@grafana/scenes';
|
import { SceneFlexItem, SceneGridItem, SceneGridLayout, SplitLayout, VizPanel } from '@grafana/scenes';
|
||||||
|
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { activateFullSceneTree } from '../utils/test-utils';
|
import { activateFullSceneTree } from '../utils/test-utils';
|
||||||
@@ -26,6 +26,37 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('PanelEditor', () => {
|
describe('PanelEditor', () => {
|
||||||
|
describe('When closing editor', () => {
|
||||||
|
it('should apply changes automatically', () => {
|
||||||
|
pluginToLoad = getTestPanelPlugin({ id: 'text', skipDataQuery: true });
|
||||||
|
|
||||||
|
const panel = new VizPanel({
|
||||||
|
key: 'panel-1',
|
||||||
|
pluginId: 'text',
|
||||||
|
});
|
||||||
|
|
||||||
|
const editScene = buildPanelEditScene(panel);
|
||||||
|
const gridItem = new SceneGridItem({ body: panel });
|
||||||
|
const scene = new DashboardScene({
|
||||||
|
editPanel: editScene,
|
||||||
|
isEditing: true,
|
||||||
|
body: new SceneGridLayout({
|
||||||
|
children: [gridItem],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const deactivate = activateFullSceneTree(scene);
|
||||||
|
|
||||||
|
const vizManager = editScene.state.panelRef.resolve();
|
||||||
|
vizManager.state.panel.setState({ title: 'changed title' });
|
||||||
|
|
||||||
|
deactivate();
|
||||||
|
|
||||||
|
const updatedPanel = gridItem.state.body as VizPanel;
|
||||||
|
expect(updatedPanel?.state.title).toBe('changed title');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('PanelDataPane', () => {
|
describe('PanelDataPane', () => {
|
||||||
it('should not exist if panel is skipDataQuery', () => {
|
it('should not exist if panel is skipDataQuery', () => {
|
||||||
pluginToLoad = getTestPanelPlugin({ id: 'text', skipDataQuery: true });
|
pluginToLoad = getTestPanelPlugin({ id: 'text', skipDataQuery: true });
|
||||||
@@ -44,7 +75,7 @@ describe('PanelEditor', () => {
|
|||||||
expect(((editScene.state.body as SplitLayout).state.primary as SplitLayout).state.secondary).toBeUndefined();
|
expect(((editScene.state.body as SplitLayout).state.primary as SplitLayout).state.secondary).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should exist if panel is supporting querying', () => {
|
it('should exist if panel is supporting querying', () => {
|
||||||
pluginToLoad = getTestPanelPlugin({ id: 'timeseries' });
|
pluginToLoad = getTestPanelPlugin({ id: 'timeseries' });
|
||||||
|
|
||||||
const panel = new VizPanel({
|
const panel = new VizPanel({
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
VizPanel,
|
VizPanel,
|
||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
|
|
||||||
import { getDashboardUrl } from '../utils/urlBuilders';
|
|
||||||
import {
|
import {
|
||||||
findVizPanelByKey,
|
findVizPanelByKey,
|
||||||
getDashboardSceneFor,
|
getDashboardSceneFor,
|
||||||
@@ -38,8 +37,18 @@ export interface PanelEditorState extends SceneObjectState {
|
|||||||
export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||||
static Component = PanelEditorRenderer;
|
static Component = PanelEditorRenderer;
|
||||||
|
|
||||||
|
private _discardChanges = false;
|
||||||
|
|
||||||
public constructor(state: PanelEditorState) {
|
public constructor(state: PanelEditorState) {
|
||||||
super(state);
|
super(state);
|
||||||
|
|
||||||
|
this.addActivationHandler(() => {
|
||||||
|
return () => {
|
||||||
|
if (!this._discardChanges) {
|
||||||
|
this.commitChanges();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
public getUrlKey() {
|
public getUrlKey() {
|
||||||
return this.state.panelId.toString();
|
return this.state.panelId.toString();
|
||||||
@@ -55,23 +64,11 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onDiscard = () => {
|
public onDiscard = () => {
|
||||||
// Open question on what to preserve when going back
|
this._discardChanges = true;
|
||||||
// Preserve time range, and variables state (that might have been changed while in panel edit)
|
locationService.partial({ editPanel: null });
|
||||||
// Preserve current panel data? (say if you just changed the time range and have new data)
|
|
||||||
this._navigateBackToDashboard();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public onApply = () => {
|
public commitChanges() {
|
||||||
this._commitChanges();
|
|
||||||
this._navigateBackToDashboard();
|
|
||||||
};
|
|
||||||
|
|
||||||
public onSave = () => {
|
|
||||||
this._commitChanges();
|
|
||||||
// Open dashboard save drawer
|
|
||||||
};
|
|
||||||
|
|
||||||
private _commitChanges() {
|
|
||||||
const dashboard = getDashboardSceneFor(this);
|
const dashboard = getDashboardSceneFor(this);
|
||||||
const sourcePanel = findVizPanelByKey(dashboard.state.body, getVizPanelKeyForPanelId(this.state.panelId));
|
const sourcePanel = findVizPanelByKey(dashboard.state.body, getVizPanelKeyForPanelId(this.state.panelId));
|
||||||
|
|
||||||
@@ -84,26 +81,6 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
|||||||
if (sourcePanel!.parent instanceof SceneGridItem) {
|
if (sourcePanel!.parent instanceof SceneGridItem) {
|
||||||
sourcePanel!.parent.setState({ body: panelMngr.state.panel.clone() });
|
sourcePanel!.parent.setState({ body: panelMngr.state.panel.clone() });
|
||||||
}
|
}
|
||||||
|
|
||||||
dashboard.setState({
|
|
||||||
isDirty: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _navigateBackToDashboard() {
|
|
||||||
const dashboard = getDashboardSceneFor(this);
|
|
||||||
locationService.push(
|
|
||||||
getDashboardUrl({
|
|
||||||
uid: dashboard.state.uid,
|
|
||||||
slug: dashboard.state.meta.slug,
|
|
||||||
currentQueryParams: locationService.getLocation().search,
|
|
||||||
updateQuery: {
|
|
||||||
editPanel: null,
|
|
||||||
// Clean the PanelEditor data pane tab query param
|
|
||||||
tab: null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import React from 'react';
|
|||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { SceneComponentProps } from '@grafana/scenes';
|
import { SceneComponentProps } from '@grafana/scenes';
|
||||||
import { Button, useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
|
|
||||||
import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator';
|
|
||||||
|
|
||||||
|
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
||||||
import { getDashboardSceneFor } from '../utils/utils';
|
import { getDashboardSceneFor } from '../utils/utils';
|
||||||
|
|
||||||
import { PanelEditor } from './PanelEditor';
|
import { PanelEditor } from './PanelEditor';
|
||||||
@@ -19,7 +18,7 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppChromeUpdate actions={getToolbarActions(model)} />
|
<NavToolbarActions dashboard={dashboard} />
|
||||||
<div className={styles.canvasContent}>
|
<div className={styles.canvasContent}>
|
||||||
{controls && (
|
{controls && (
|
||||||
<div className={styles.controls}>
|
<div className={styles.controls}>
|
||||||
@@ -36,29 +35,6 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getToolbarActions(editor: PanelEditor) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<NavToolbarSeparator leftActionsSeparator key="separator" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={editor.onDiscard}
|
|
||||||
tooltip=""
|
|
||||||
key="panel-edit-discard"
|
|
||||||
variant="destructive"
|
|
||||||
fill="outline"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
Discard
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button onClick={editor.onApply} tooltip="" key="panel-edit-apply" variant="primary" size="sm">
|
|
||||||
Apply
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStyles(theme: GrafanaTheme2) {
|
function getStyles(theme: GrafanaTheme2) {
|
||||||
return {
|
return {
|
||||||
canvasContent: css({
|
canvasContent: css({
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { MultiValueVariable, sceneGraph } from '@grafana/scenes';
|
import { MultiValueVariable, sceneGraph } from '@grafana/scenes';
|
||||||
|
|
||||||
|
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
|
||||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||||
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
|
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
|
||||||
|
import { findVizPanelByKey } from '../utils/utils';
|
||||||
|
|
||||||
import { getSaveDashboardChange } from './getSaveDashboardChange';
|
import { getSaveDashboardChange } from './getSaveDashboardChange';
|
||||||
|
|
||||||
@@ -59,15 +61,43 @@ describe('getSaveDashboardChange', () => {
|
|||||||
expect(result.hasChanges).toBe(true);
|
expect(result.hasChanges).toBe(true);
|
||||||
expect(result.diffCount).toBe(2);
|
expect(result.diffCount).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Saving from panel edit', () => {
|
||||||
|
it('Should commit panel edit changes', () => {
|
||||||
|
const dashboard = setup();
|
||||||
|
const panel = findVizPanelByKey(dashboard, 'panel-1')!;
|
||||||
|
const editScene = buildPanelEditScene(panel);
|
||||||
|
|
||||||
|
dashboard.onEnterEditMode();
|
||||||
|
dashboard.setState({ editPanel: editScene });
|
||||||
|
|
||||||
|
const vizManager = editScene.state.panelRef.resolve();
|
||||||
|
vizManager.state.panel.setState({ title: 'changed title' });
|
||||||
|
|
||||||
|
const result = getSaveDashboardChange(dashboard, false, true);
|
||||||
|
const panelSaveModel = result.changedSaveModel.panels![0];
|
||||||
|
expect(panelSaveModel.title).toBe('changed title');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function setup() {
|
interface ScenarioOptions {
|
||||||
|
fromPanelEdit?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup(options: ScenarioOptions = {}) {
|
||||||
const dashboard = transformSaveModelToScene({
|
const dashboard = transformSaveModelToScene({
|
||||||
dashboard: {
|
dashboard: {
|
||||||
title: 'hello',
|
title: 'hello',
|
||||||
uid: 'my-uid',
|
uid: 'my-uid',
|
||||||
schemaVersion: 30,
|
schemaVersion: 30,
|
||||||
panels: [],
|
panels: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Panel 1',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
version: 10,
|
version: 10,
|
||||||
templating: {
|
templating: {
|
||||||
list: [
|
list: [
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ export function getSaveDashboardChange(
|
|||||||
saveVariables?: boolean
|
saveVariables?: boolean
|
||||||
): DashboardChangeInfo {
|
): DashboardChangeInfo {
|
||||||
const initialSaveModel = dashboard.getInitialSaveModel()!;
|
const initialSaveModel = dashboard.getInitialSaveModel()!;
|
||||||
|
|
||||||
|
if (dashboard.state.editPanel) {
|
||||||
|
dashboard.state.editPanel.commitChanges();
|
||||||
|
}
|
||||||
|
|
||||||
const changedSaveModel = transformSceneToSaveModel(dashboard);
|
const changedSaveModel = transformSceneToSaveModel(dashboard);
|
||||||
const hasTimeChanged = getHasTimeChanged(changedSaveModel, initialSaveModel);
|
const hasTimeChanged = getHasTimeChanged(changedSaveModel, initialSaveModel);
|
||||||
|
|
||||||
|
|||||||
@@ -36,14 +36,16 @@ NavToolbarActions.displayName = 'NavToolbarActions';
|
|||||||
* This part is split into a separate componet to help test this
|
* This part is split into a separate componet to help test this
|
||||||
*/
|
*/
|
||||||
export function ToolbarActions({ dashboard }: Props) {
|
export function ToolbarActions({ dashboard }: Props) {
|
||||||
const { isEditing, viewPanelScene, isDirty, uid, meta, editview } = dashboard.useState();
|
const { isEditing, viewPanelScene, isDirty, uid, meta, editview, editPanel } = dashboard.useState();
|
||||||
const canSaveAs = contextSrv.hasEditPermissionInFolders;
|
const canSaveAs = contextSrv.hasEditPermissionInFolders;
|
||||||
const toolbarActions: ToolbarAction[] = [];
|
const toolbarActions: ToolbarAction[] = [];
|
||||||
const buttonWithExtraMargin = useStyles2(getStyles);
|
const buttonWithExtraMargin = useStyles2(getStyles);
|
||||||
|
const isEditingPanel = Boolean(editPanel);
|
||||||
|
const isViewingPanel = Boolean(viewPanelScene);
|
||||||
|
|
||||||
toolbarActions.push({
|
toolbarActions.push({
|
||||||
group: 'icon-actions',
|
group: 'icon-actions',
|
||||||
condition: uid && !editview && Boolean(meta.canStar),
|
condition: uid && !editview && Boolean(meta.canStar) && !isEditingPanel,
|
||||||
render: () => {
|
render: () => {
|
||||||
let desc = meta.isStarred
|
let desc = meta.isStarred
|
||||||
? t('dashboard.toolbar.unmark-favorite', 'Unmark as favorite')
|
? t('dashboard.toolbar.unmark-favorite', 'Unmark as favorite')
|
||||||
@@ -66,7 +68,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
|||||||
|
|
||||||
toolbarActions.push({
|
toolbarActions.push({
|
||||||
group: 'icon-actions',
|
group: 'icon-actions',
|
||||||
condition: uid && !editview,
|
condition: uid && !editview && !isEditingPanel,
|
||||||
render: () => (
|
render: () => (
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
key="view-in-old-dashboard-button"
|
key="view-in-old-dashboard-button"
|
||||||
@@ -98,7 +100,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dynamicDashNavActions.left.length > 0) {
|
if (dynamicDashNavActions.left.length > 0 && !isEditingPanel) {
|
||||||
dynamicDashNavActions.left.map((action, index) => {
|
dynamicDashNavActions.left.map((action, index) => {
|
||||||
const props = { dashboard: getDashboardSrv().getCurrent()! };
|
const props = { dashboard: getDashboardSrv().getCurrent()! };
|
||||||
if (action.show(props)) {
|
if (action.show(props)) {
|
||||||
@@ -114,11 +116,11 @@ export function ToolbarActions({ dashboard }: Props) {
|
|||||||
|
|
||||||
toolbarActions.push({
|
toolbarActions.push({
|
||||||
group: 'back-button',
|
group: 'back-button',
|
||||||
condition: Boolean(viewPanelScene),
|
condition: isViewingPanel || isEditingPanel,
|
||||||
render: () => (
|
render: () => (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
locationService.partial({ viewPanel: null });
|
locationService.partial({ viewPanel: null, editPanel: null });
|
||||||
}}
|
}}
|
||||||
tooltip=""
|
tooltip=""
|
||||||
key="back"
|
key="back"
|
||||||
@@ -173,7 +175,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
|||||||
|
|
||||||
toolbarActions.push({
|
toolbarActions.push({
|
||||||
group: 'main-buttons',
|
group: 'main-buttons',
|
||||||
condition: !isEditing && dashboard.canEditDashboard() && !viewPanelScene,
|
condition: !isEditing && dashboard.canEditDashboard() && !isViewingPanel && !isEditingPanel,
|
||||||
render: () => (
|
render: () => (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -192,7 +194,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
|||||||
|
|
||||||
toolbarActions.push({
|
toolbarActions.push({
|
||||||
group: 'settings',
|
group: 'settings',
|
||||||
condition: isEditing && dashboard.canEditDashboard() && !viewPanelScene && !editview,
|
condition: isEditing && dashboard.canEditDashboard() && !isViewingPanel && !isEditingPanel && !editview,
|
||||||
render: () => (
|
render: () => (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -211,7 +213,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
|||||||
|
|
||||||
toolbarActions.push({
|
toolbarActions.push({
|
||||||
group: 'main-buttons',
|
group: 'main-buttons',
|
||||||
condition: isEditing && !editview && !meta.isNew,
|
condition: isEditing && !editview && !meta.isNew && !isViewingPanel && !isEditingPanel,
|
||||||
render: () => (
|
render: () => (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => dashboard.exitEditMode({ skipConfirm: false })}
|
onClick={() => dashboard.exitEditMode({ skipConfirm: false })}
|
||||||
@@ -226,6 +228,23 @@ export function ToolbarActions({ dashboard }: Props) {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
toolbarActions.push({
|
||||||
|
group: 'main-buttons',
|
||||||
|
condition: isEditingPanel && !editview && !meta.isNew && !isViewingPanel,
|
||||||
|
render: () => (
|
||||||
|
<Button
|
||||||
|
onClick={editPanel?.onDiscard}
|
||||||
|
tooltip="Discard panel changes"
|
||||||
|
size="sm"
|
||||||
|
key="discard"
|
||||||
|
fill="outline"
|
||||||
|
variant="destructive"
|
||||||
|
>
|
||||||
|
Discard panel changes
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
toolbarActions.push({
|
toolbarActions.push({
|
||||||
group: 'main-buttons',
|
group: 'main-buttons',
|
||||||
condition: isEditing && (meta.canSave || canSaveAs),
|
condition: isEditing && (meta.canSave || canSaveAs),
|
||||||
|
|||||||
Reference in New Issue
Block a user