Dashboard: Panel edit undo/redo and other edit actions should trigger repeat of grid/auto items (#110606)
* Panel edit undo-redo * Update * Update * fix lint & test * Try switching to source tab * Added temp fix for old editing ux * Update
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import { SceneObjectState, SceneObjectBase, SceneObject, sceneGraph, VizPanel } from '@grafana/scenes';
|
||||
import { SceneObjectState, SceneObjectBase, SceneObject, sceneGraph } from '@grafana/scenes';
|
||||
import {
|
||||
ElementSelectionContextItem,
|
||||
ElementSelectionContextState,
|
||||
ElementSelectionOnSelectOptions,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem';
|
||||
import { TabItem } from '../scene/layout-tabs/TabItem';
|
||||
import { isRepeatCloneOrChildOf } from '../utils/clone';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
@@ -44,6 +44,12 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
|
||||
this.addActivationHandler(this.onActivate.bind(this));
|
||||
}
|
||||
|
||||
private panelEditAction?: DashboardEditActionEvent;
|
||||
|
||||
public setPanelEditAction(editAction: DashboardEditActionEvent) {
|
||||
this.panelEditAction = editAction;
|
||||
}
|
||||
|
||||
private onActivate() {
|
||||
const dashboard = getDashboardSceneFor(this);
|
||||
|
||||
@@ -76,6 +82,22 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
|
||||
this.forceRender();
|
||||
})
|
||||
);
|
||||
|
||||
if (this.panelEditAction) {
|
||||
this.performPanelEditAction(this.panelEditAction);
|
||||
this.panelEditAction = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private performPanelEditAction(action: DashboardEditActionEvent) {
|
||||
// Some layout items are not yet active when leaving panel edit, let's wait for them to activate
|
||||
if (!action.payload.source.isActive) {
|
||||
trySwitchingToSourceTab(action.payload.source);
|
||||
setTimeout(() => this.performPanelEditAction(action));
|
||||
return;
|
||||
}
|
||||
|
||||
action.payload.source.publishEvent(action, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,15 +115,6 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
|
||||
this.performAction(action);
|
||||
|
||||
this.setState({ undoStack: [...this.state.undoStack, action] });
|
||||
|
||||
// Notify repeaters that something changed
|
||||
if (action.source instanceof VizPanel) {
|
||||
const layoutElement = action.source.parent!;
|
||||
|
||||
if (isDashboardLayoutItem(layoutElement) && layoutElement.editingCompleted) {
|
||||
layoutElement.editingCompleted(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,3 +271,19 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
|
||||
this.state.selection?.markAsNewElement();
|
||||
}
|
||||
}
|
||||
|
||||
function trySwitchingToSourceTab(source: SceneObject) {
|
||||
if (source.parent === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.parent instanceof TabItem) {
|
||||
const tab = source.parent;
|
||||
const tabsLayout = source.parent.getParentLayout();
|
||||
if (tabsLayout.state.currentTabSlug !== tab.getSlug()) {
|
||||
tabsLayout.switchToTab(tab);
|
||||
}
|
||||
} else {
|
||||
trySwitchingToSourceTab(source.parent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { DashboardEditActionEvent } from '../edit-pane/shared';
|
||||
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
|
||||
import { getPanelChanges } from '../saving/getDashboardChanges';
|
||||
import { UNCONFIGURED_PANEL_PLUGIN_ID } from '../scene/UnconfiguredPanel';
|
||||
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
|
||||
import { DashboardLayoutItem, isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem';
|
||||
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
||||
import {
|
||||
@@ -96,13 +97,50 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
this.waitForPlugin();
|
||||
|
||||
return () => {
|
||||
this._layoutItem.editingCompleted?.(this.state.isDirty || this._changesHaveBeenMade);
|
||||
this.commitChanges();
|
||||
|
||||
if (deactivateParents) {
|
||||
deactivateParents();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private commitChanges() {
|
||||
if (!this.state.isDirty && !this._changesHaveBeenMade) {
|
||||
// Nothing to commit
|
||||
return;
|
||||
}
|
||||
|
||||
const layoutItem = this._layoutItem;
|
||||
const changedState = layoutItem.state;
|
||||
const originalState = this._layoutItemState!;
|
||||
|
||||
// Temp fix for old edit mode
|
||||
if (this._layoutItem instanceof DashboardGridItem && !config.featureToggles.dashboardNewLayouts) {
|
||||
this._layoutItem.handleEditChange();
|
||||
return;
|
||||
}
|
||||
|
||||
const editAction = new DashboardEditActionEvent({
|
||||
description: t('dashboard.edit-actions.panel-edit', 'Panel changes'),
|
||||
source: this._layoutItem,
|
||||
perform: () => {
|
||||
// Because panel edit makes changes directly to layout item & panel
|
||||
// we only need to do this in case we want to re-perform after undo
|
||||
if (layoutItem.state !== changedState) {
|
||||
layoutItem.setState(changedState);
|
||||
}
|
||||
},
|
||||
undo: () => layoutItem!.setState(originalState),
|
||||
});
|
||||
|
||||
// sadly we cannot publish this event directly here as the main dashboard edit / undo system
|
||||
// is not active while panel edit is active so we have to let the edit pane (which owns undo/redo)
|
||||
// publish this event when it activates
|
||||
const dashboard = getDashboardSceneFor(this);
|
||||
dashboard.state.editPane.setPanelEditAction(editAction);
|
||||
}
|
||||
|
||||
private waitForPlugin(retry = 0) {
|
||||
const panel = this.getPanel();
|
||||
const plugin = panel.getPlugin();
|
||||
@@ -166,8 +204,6 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
if (this.state.isInitializing) {
|
||||
this.setOriginalState(this.state.panelRef);
|
||||
|
||||
this._layoutItem.editingStarted?.();
|
||||
|
||||
this._setupChangeDetection();
|
||||
this._updateDataPane(plugin);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
|
||||
import { ConditionalRenderingGroup } from '../../conditional-rendering/group/ConditionalRenderingGroup';
|
||||
import { DashboardStateChangedEvent } from '../../edit-pane/shared';
|
||||
import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone';
|
||||
import { getMultiVariableValues } from '../../utils/utils';
|
||||
import { scrollCanvasElementIntoView } from '../layouts-shared/scrollCanvasElementIntoView';
|
||||
@@ -54,6 +55,8 @@ export class AutoGridItem extends SceneObjectBase<AutoGridItemState> implements
|
||||
this.performRepeat();
|
||||
}
|
||||
|
||||
this._subs.add(this.subscribeToEvent(DashboardStateChangedEvent, () => this.handleEditChange()));
|
||||
|
||||
const deactivate = this.state.conditionalRendering?.activate();
|
||||
|
||||
return () => {
|
||||
@@ -167,16 +170,8 @@ export class AutoGridItem extends SceneObjectBase<AutoGridItemState> implements
|
||||
};
|
||||
}
|
||||
|
||||
public editingStarted() {
|
||||
if (!this.state.variableName) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public editingCompleted(withChanges: boolean) {
|
||||
if (withChanges) {
|
||||
this._prevRepeatValues = undefined;
|
||||
}
|
||||
public handleEditChange() {
|
||||
this._prevRepeatValues = undefined;
|
||||
|
||||
this.performRepeat();
|
||||
}
|
||||
|
||||
+2
-77
@@ -3,6 +3,7 @@ import { setPluginImportUtils } from '@grafana/runtime';
|
||||
import { SceneGridLayout, SceneVariableSet, TestVariable, VizPanel } from '@grafana/scenes';
|
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants';
|
||||
|
||||
import { DashboardEditActionEvent } from '../../edit-pane/shared';
|
||||
import { activateFullSceneTree, buildPanelRepeaterScene } from '../../utils/test-utils';
|
||||
import { DashboardScene } from '../DashboardScene';
|
||||
|
||||
@@ -130,89 +131,13 @@ describe('PanelRepeaterGridItem', () => {
|
||||
|
||||
vizPanel.setState({ title: 'Changed' });
|
||||
|
||||
panel.editingCompleted(true);
|
||||
|
||||
// mimic returning to dashboard
|
||||
activateFullSceneTree(scene);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
|
||||
expect(panel.state.repeatedPanels?.length).toBe(4);
|
||||
expect((panel.state.repeatedPanels![0] as VizPanel).state.title).toBe('Changed');
|
||||
});
|
||||
|
||||
it('Should only redo the repeat of an edited panel, not all panels in dashboard', async () => {
|
||||
const panel = new DashboardGridItem({
|
||||
variableName: 'server',
|
||||
repeatedPanels: [],
|
||||
body: new VizPanel({
|
||||
title: 'Panel $server',
|
||||
}),
|
||||
});
|
||||
|
||||
const panel2 = new DashboardGridItem({
|
||||
variableName: 'server',
|
||||
repeatedPanels: [],
|
||||
body: new VizPanel({
|
||||
title: 'Panel $server 2',
|
||||
}),
|
||||
});
|
||||
|
||||
const variable = new TestVariable({
|
||||
name: 'server',
|
||||
query: 'A.*',
|
||||
value: ALL_VARIABLE_VALUE,
|
||||
text: ALL_VARIABLE_TEXT,
|
||||
isMulti: true,
|
||||
includeAll: true,
|
||||
delayMs: 0,
|
||||
optionsToReturn: [
|
||||
{ label: 'A', value: '1' },
|
||||
{ label: 'B', value: '2' },
|
||||
{ label: 'C', value: '3' },
|
||||
{ label: 'D', value: '4' },
|
||||
{ label: 'E', value: '5' },
|
||||
],
|
||||
});
|
||||
|
||||
const scene = new DashboardScene({
|
||||
$variables: new SceneVariableSet({
|
||||
variables: [variable],
|
||||
}),
|
||||
body: new DefaultGridLayoutManager({
|
||||
grid: new SceneGridLayout({
|
||||
children: [panel, panel2],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
const deactivate = activateFullSceneTree(scene);
|
||||
panel.publishEvent(new DashboardEditActionEvent({ source: panel, perform: () => {}, undo: () => {} }), true);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
|
||||
expect(panel.state.repeatedPanels?.length).toBe(4);
|
||||
|
||||
const vizPanel = panel.state.body as VizPanel;
|
||||
|
||||
expect(vizPanel.state.title).toBe('Panel $server');
|
||||
|
||||
// mimic going to panel edit
|
||||
deactivate();
|
||||
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
|
||||
vizPanel.setState({ title: 'Changed' });
|
||||
|
||||
panel.editingCompleted(true);
|
||||
|
||||
const performRepeatMock = jest.spyOn(panel, 'performRepeat');
|
||||
|
||||
// mimic returning to dashboard
|
||||
activateFullSceneTree(scene);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
|
||||
expect(performRepeatMock).toHaveBeenCalledTimes(1); // only for the edited panel
|
||||
expect(panel.state.repeatedPanels?.length).toBe(4);
|
||||
expect((panel.state.repeatedPanels![0] as VizPanel).state.title).toBe('Changed');
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { GRID_COLUMN_COUNT } from 'app/core/constants';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
|
||||
import { DashboardStateChangedEvent } from '../../edit-pane/shared';
|
||||
import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone';
|
||||
import { getMultiVariableValues } from '../../utils/utils';
|
||||
import { scrollCanvasElementIntoView, scrollIntoView } from '../layouts-shared/scrollCanvasElementIntoView';
|
||||
@@ -61,6 +62,8 @@ export class DashboardGridItem
|
||||
private _activationHandler() {
|
||||
this.handleVariableName();
|
||||
|
||||
this._subs.add(this.subscribeToEvent(DashboardStateChangedEvent, () => this.handleEditChange()));
|
||||
|
||||
return () => {
|
||||
this._handleGridSizeUnsubscribe();
|
||||
};
|
||||
@@ -114,26 +117,21 @@ export class DashboardGridItem
|
||||
this.setState({ body });
|
||||
}
|
||||
|
||||
public editingStarted() {
|
||||
if (!this.state.variableName) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
public handleEditChange() {
|
||||
this._prevRepeatValues = undefined;
|
||||
|
||||
public editingCompleted(withChanges: boolean) {
|
||||
if (withChanges) {
|
||||
this._prevRepeatValues = undefined;
|
||||
if (this.parent instanceof SceneGridRow) {
|
||||
const repeater = this.parent.state.$behaviors?.find((b) => b instanceof RowRepeaterBehavior);
|
||||
if (repeater) {
|
||||
repeater.resetPrevRepeatValues();
|
||||
}
|
||||
if (this.parent instanceof SceneGridRow) {
|
||||
const repeater = this.parent.state.$behaviors?.find((b) => b instanceof RowRepeaterBehavior);
|
||||
if (repeater) {
|
||||
repeater.resetPrevRepeatValues();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.variableName && this.state.repeatDirection === 'h' && this.state.width !== GRID_COLUMN_COUNT) {
|
||||
this.setState({ width: GRID_COLUMN_COUNT });
|
||||
}
|
||||
|
||||
this.performRepeat();
|
||||
}
|
||||
|
||||
public performRepeat() {
|
||||
|
||||
@@ -15,16 +15,6 @@ export interface DashboardLayoutItem extends SceneObject {
|
||||
*/
|
||||
getOptions?(): OptionsPaneCategoryDescriptor[];
|
||||
|
||||
/**
|
||||
* When going into panel edit
|
||||
**/
|
||||
editingStarted?(): void;
|
||||
|
||||
/**
|
||||
* When coming out of panel edit
|
||||
*/
|
||||
editingCompleted?(withChanges: boolean): void;
|
||||
|
||||
/**
|
||||
* Change inner body / viz panel
|
||||
*/
|
||||
|
||||
@@ -4681,6 +4681,7 @@
|
||||
"move": "Move {{typeName}}",
|
||||
"panel-background": "Change panel background",
|
||||
"panel-description": "Change panel description",
|
||||
"panel-edit": "Panel changes",
|
||||
"panel-max-repeats-per-row": "Max repeats per row",
|
||||
"panel-repeat-direction": "Repeat direction",
|
||||
"panel-repeat-variable": "Panel repeat by",
|
||||
|
||||
Reference in New Issue
Block a user