Dashboards: Add undo/redo actions for changing dashboard title (#106574)

* Dashboards: Add undo/redo actions for changing dashboard title

* Run make i18n-extract

* Prevent empty undo/redo actions
This commit is contained in:
kay delaney
2025-06-13 15:05:27 +01:00
committed by GitHub
parent 5f21f320f7
commit 3c0d087186
4 changed files with 66 additions and 6 deletions
@@ -1,4 +1,4 @@
import { ReactNode, useMemo } from 'react';
import { ReactNode, useMemo, useRef } from 'react';
import { Trans, t } from '@grafana/i18n';
import { Button, Input, TextArea } from '@grafana/ui';
@@ -7,9 +7,12 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan
import { DashboardScene } from '../scene/DashboardScene';
import { useLayoutCategory } from '../scene/layouts-shared/DashboardLayoutSelector';
import { redoButtonId, undoButtonID } from '../scene/new-toolbar/RightActions';
import { EditSchemaV2Button } from '../scene/new-toolbar/actions/EditSchemaV2Button';
import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement';
import { dashboardEditActions } from './shared';
export class DashboardEditableElement implements EditableDashboardElement {
public readonly isEditableDashboardElement = true;
@@ -79,7 +82,38 @@ export class DashboardEditableElement implements EditableDashboardElement {
export function DashboardTitleInput({ dashboard, id }: { dashboard: DashboardScene; id?: string }) {
const { title } = dashboard.useState();
return <Input id={id} value={title} onChange={(e) => dashboard.setState({ title: e.currentTarget.value })} />;
// We want to save the unchanged value for the 'undo' action
const valueBeforeEdit = useRef('');
return (
<Input
id={id}
value={title}
onChange={(e) => {
dashboard.setState({ title: e.currentTarget.value });
}}
onFocus={(e) => {
valueBeforeEdit.current = e.currentTarget.value;
}}
onBlur={(e) => {
// If the title input is currently focused and we click undo/redo
// we don't want to mess with the stack
const clickedUndoRedo =
e.relatedTarget && (e.relatedTarget.id === undoButtonID || e.relatedTarget.id === redoButtonId);
const titleUnchanged = valueBeforeEdit.current === e.currentTarget.value;
const shouldSkip = titleUnchanged || clickedUndoRedo;
if (shouldSkip) {
return;
}
dashboardEditActions.changeTitle({
source: dashboard,
oldTitle: valueBeforeEdit.current,
newTitle: e.currentTarget.value,
});
}}
/>
);
}
export function DashboardDescriptionInput({ dashboard, id }: { dashboard: DashboardScene; id?: string }) {
@@ -98,17 +98,23 @@ export interface RemoveElementActionHelperProps {
undo: () => void;
}
export interface ChangeTitleActionHelperProps {
oldTitle: string;
newTitle: string;
source: DashboardScene;
}
export const dashboardEditActions = {
/**
* Registers and peforms an edit action
*/
edit: function (props: DashboardEditActionEventPayload) {
edit(props: DashboardEditActionEventPayload) {
props.source.publishEvent(new DashboardEditActionEvent(props), true);
},
/**
* Helper for makeEdit that adds elements
*/
addElement: function (props: AddElementActionHelperProps) {
addElement(props: AddElementActionHelperProps) {
const { addedObject, source, perform, undo } = props;
const element = getEditableElementFor(addedObject);
@@ -145,4 +151,17 @@ export const dashboardEditActions = {
undo,
});
},
changeTitle({ source, oldTitle, newTitle }: ChangeTitleActionHelperProps) {
dashboardEditActions.edit({
description: t('dashboard.title.action', 'Change dashboard title'),
source: source,
perform: () => {
source.setState({ title: newTitle });
},
undo: () => {
source.setState({ title: oldTitle });
},
});
},
};
@@ -165,14 +165,16 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => {
);
};
export const undoButtonID = 'undo-button';
function UndoButton({ dashboard }: ToolbarActionProps) {
const editPane = dashboard.state.editPane;
const { undoStack } = editPane.useState();
const undoAction = undoStack[undoStack.length - 1];
const tooltip = undoAction ? `Undo '${undoAction.description}'` : 'Undo';
const tooltip = `Undo${undoAction?.description ? ` '${undoAction.description}'` : ''}`;
return (
<ToolbarButton
id={undoButtonID}
icon="corner-up-left"
disabled={undoStack.length === 0}
onClick={() => editPane.undoAction()}
@@ -181,14 +183,16 @@ function UndoButton({ dashboard }: ToolbarActionProps) {
);
}
export const redoButtonId = 'redo-button';
function RedoButton({ dashboard }: ToolbarActionProps) {
const editPane = dashboard.state.editPane;
const { redoStack } = editPane.useState();
const redoAction = redoStack[redoStack.length - 1];
const tooltip = redoAction ? `Redo '${redoAction?.description}'` : 'Redo';
const tooltip = `Redo${redoAction?.description ? ` '${redoAction.description}'` : ''}`;
return (
<ToolbarButton
id={redoButtonId}
icon="corner-up-right"
disabled={redoStack.length === 0}
tooltip={tooltip}
+3
View File
@@ -4606,6 +4606,9 @@
"title-not-unique": "This title is not unique"
}
},
"title": {
"action": "Change dashboard title"
},
"toolbar": {
"add": "Add",
"alert-rules": "Alert rules",