From 3c0d0871866f861532c69433d6de34e9eef7ba89 Mon Sep 17 00:00:00 2001
From: kay delaney <45561153+kaydelaney@users.noreply.github.com>
Date: Fri, 13 Jun 2025 15:05:27 +0100
Subject: [PATCH] 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
---
.../edit-pane/DashboardEditableElement.tsx | 38 ++++++++++++++++++-
.../dashboard-scene/edit-pane/shared.ts | 23 ++++++++++-
.../scene/new-toolbar/RightActions.tsx | 8 +++-
public/locales/en-US/grafana.json | 3 ++
4 files changed, 66 insertions(+), 6 deletions(-)
diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx
index d3df22e439c..7e392b04eb8 100644
--- a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx
+++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx
@@ -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 dashboard.setState({ title: e.currentTarget.value })} />;
+ // We want to save the unchanged value for the 'undo' action
+ const valueBeforeEdit = useRef('');
+
+ return (
+ {
+ 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 }) {
diff --git a/public/app/features/dashboard-scene/edit-pane/shared.ts b/public/app/features/dashboard-scene/edit-pane/shared.ts
index 33ef498e402..a2bd2c94544 100644
--- a/public/app/features/dashboard-scene/edit-pane/shared.ts
+++ b/public/app/features/dashboard-scene/edit-pane/shared.ts
@@ -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 });
+ },
+ });
+ },
};
diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx
index 721c3396a28..76cb969eeaf 100644
--- a/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx
+++ b/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx
@@ -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 (
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 (