mirror of https://github.com/grafana/grafana.git
Dashboards: Add undo/redo actions for several template variable options (#106818)
* Dashboards: Add undo/redo actions for several template variable options Also refactors some existing undo/redo code * Run `make i18n-extract` * formatting
This commit is contained in:
parent
39b9700048
commit
51a5b0ab65
|
@ -108,8 +108,8 @@ export function DashboardTitleInput({ dashboard, id }: { dashboard: DashboardSce
|
|||
|
||||
dashboardEditActions.changeTitle({
|
||||
source: dashboard,
|
||||
oldTitle: valueBeforeEdit.current,
|
||||
newTitle: e.currentTarget.value,
|
||||
oldValue: valueBeforeEdit.current,
|
||||
newValue: e.currentTarget.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
@ -139,8 +139,8 @@ export function DashboardDescriptionInput({ dashboard, id }: { dashboard: Dashbo
|
|||
|
||||
dashboardEditActions.changeDescription({
|
||||
source: dashboard,
|
||||
oldDescription: valueBeforeEdit.current,
|
||||
newDescription: e.currentTarget.value,
|
||||
oldValue: valueBeforeEdit.current,
|
||||
newValue: e.currentTarget.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @grafana/i18n/no-translation-top-level */
|
||||
import { useSessionStorage } from 'react-use';
|
||||
|
||||
import { BusEventWithPayload } from '@grafana/data';
|
||||
|
@ -191,6 +192,15 @@ export const dashboardEditActions = {
|
|||
});
|
||||
},
|
||||
|
||||
changeTitle: makeEditAction<DashboardScene, 'title'>({
|
||||
description: t('dashboard.title.action', 'Change dashboard title'),
|
||||
prop: 'title',
|
||||
}),
|
||||
changeDescription: makeEditAction<DashboardScene, 'description'>({
|
||||
description: t('dashboard.description.action', 'Change dashboard description'),
|
||||
prop: 'description',
|
||||
}),
|
||||
|
||||
addVariable({ source, addedObject }: AddVariableActionHelperProps) {
|
||||
const varsBeforeAddition = [...source.state.variables];
|
||||
|
||||
|
@ -219,32 +229,22 @@ export const dashboardEditActions = {
|
|||
},
|
||||
});
|
||||
},
|
||||
|
||||
changeTitle({ source, oldTitle, newTitle }: ChangeTitleActionHelperProps) {
|
||||
dashboardEditActions.edit({
|
||||
description: t('dashboard.title.action', 'Change dashboard title'),
|
||||
source,
|
||||
perform: () => {
|
||||
source.setState({ title: newTitle });
|
||||
},
|
||||
undo: () => {
|
||||
source.setState({ title: oldTitle });
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
changeDescription({ source, oldDescription, newDescription }: ChangeDescriptionActionHelperProps) {
|
||||
dashboardEditActions.edit({
|
||||
description: t('dashboard.description.action', 'Change dashboard description'),
|
||||
source,
|
||||
perform: () => {
|
||||
source.setState({ description: newDescription });
|
||||
},
|
||||
undo: () => {
|
||||
source.setState({ description: oldDescription });
|
||||
},
|
||||
});
|
||||
},
|
||||
changeVariableName: makeEditAction<SceneVariable, 'name'>({
|
||||
description: t('dashboard.variable.name.action', 'Change variable name'),
|
||||
prop: 'name',
|
||||
}),
|
||||
changeVariableLabel: makeEditAction<SceneVariable, 'label'>({
|
||||
description: t('dashboard.variable.label.action', 'Change variable label'),
|
||||
prop: 'label',
|
||||
}),
|
||||
changeVariableDescription: makeEditAction<SceneVariable, 'description'>({
|
||||
description: t('dashboard.variable.description.action', 'Change variable description'),
|
||||
prop: 'description',
|
||||
}),
|
||||
changeVariableHideValue: makeEditAction<SceneVariable, 'hide'>({
|
||||
description: t('dashboard.variable.hide.action', 'Change variable hide option'),
|
||||
prop: 'hide',
|
||||
}),
|
||||
|
||||
moveElement(props: MoveElementActionHelperProps) {
|
||||
const { movedObject, source, perform, undo } = props;
|
||||
|
@ -266,6 +266,35 @@ export const dashboardEditActions = {
|
|||
},
|
||||
};
|
||||
|
||||
interface MakeEditActionProps<Source extends SceneObject, T extends keyof Source['state']> {
|
||||
description: string;
|
||||
prop: T;
|
||||
}
|
||||
|
||||
interface EditActionProps<Source extends SceneObject, T extends keyof Source['state']> {
|
||||
source: Source;
|
||||
oldValue: Source['state'][T];
|
||||
newValue: Source['state'][T];
|
||||
}
|
||||
|
||||
function makeEditAction<Source extends SceneObject, T extends keyof Source['state']>({
|
||||
description,
|
||||
prop,
|
||||
}: MakeEditActionProps<Source, T>) {
|
||||
return ({ source, oldValue, newValue }: EditActionProps<Source, T>) => {
|
||||
dashboardEditActions.edit({
|
||||
description,
|
||||
source,
|
||||
perform: () => {
|
||||
source.setState({ [prop]: newValue });
|
||||
},
|
||||
undo: () => {
|
||||
source.setState({ [prop]: oldValue });
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function undoRedoWasClicked(e: React.FocusEvent) {
|
||||
return e.relatedTarget && (e.relatedTarget.id === undoButtonID || e.relatedTarget.id === redoButtonId);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { FormEvent, useMemo, useState } from 'react';
|
||||
import { FormEvent, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { VariableHide } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
@ -9,7 +9,7 @@ import { Input, TextArea, Button, Field, Box, Stack } from '@grafana/ui';
|
|||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
import { dashboardEditActions } from '../../edit-pane/shared';
|
||||
import { dashboardEditActions, undoRedoWasClicked } from '../../edit-pane/shared';
|
||||
import { useEditPaneInputAutoFocus } from '../../scene/layouts-shared/utils';
|
||||
import { BulkActionElement } from '../../scene/types/BulkActionElement';
|
||||
import { EditableDashboardElement, EditableDashboardElementInfo } from '../../scene/types/EditableDashboardElement';
|
||||
|
@ -123,36 +123,49 @@ function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable
|
|||
const { name } = variable.useState();
|
||||
const ref = useEditPaneInputAutoFocus({ autoFocus: isNewElement });
|
||||
const [nameError, setNameError] = useState<string>();
|
||||
const [validName, setValidName] = useState<string>(variable.state.name);
|
||||
|
||||
const onChange = (e: FormEvent<HTMLInputElement>) => {
|
||||
const result = validateVariableName(variable, e.currentTarget.value);
|
||||
if (result.errorMessage !== nameError) {
|
||||
setNameError(result.errorMessage);
|
||||
} else {
|
||||
setValidName(variable.state.name);
|
||||
}
|
||||
|
||||
variable.setState({ name: e.currentTarget.value });
|
||||
};
|
||||
|
||||
// Restore valid name if bluring while invalid
|
||||
const onBlur = () => {
|
||||
if (nameError) {
|
||||
variable.setState({ name: validName });
|
||||
setNameError(undefined);
|
||||
}
|
||||
};
|
||||
const oldName = useRef(name);
|
||||
|
||||
return (
|
||||
<Field label={t('dashboard.edit-pane.variable.name', 'Name')} invalid={!!nameError} error={nameError}>
|
||||
<Input
|
||||
ref={ref}
|
||||
value={name}
|
||||
onFocus={() => {
|
||||
oldName.current = name;
|
||||
}}
|
||||
onChange={onChange}
|
||||
required
|
||||
onBlur={onBlur}
|
||||
onBlur={(e) => {
|
||||
const labelUnchanged = oldName.current === name;
|
||||
const shouldSkip = labelUnchanged || undoRedoWasClicked(e);
|
||||
|
||||
if (nameError) {
|
||||
setNameError(undefined);
|
||||
variable.setState({ name: oldName.current });
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldSkip) {
|
||||
return;
|
||||
}
|
||||
|
||||
dashboardEditActions.changeVariableName({
|
||||
source: variable,
|
||||
oldValue: oldName.current,
|
||||
newValue: name,
|
||||
});
|
||||
}}
|
||||
data-testid={selectors.components.PanelEditor.ElementEditPane.variableNameInput}
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
|
@ -160,10 +173,29 @@ function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable
|
|||
|
||||
function VariableLabelInput({ variable }: VariableInputProps) {
|
||||
const { label } = variable.useState();
|
||||
const oldLabel = useRef(label ?? '');
|
||||
|
||||
return (
|
||||
<Input
|
||||
value={label}
|
||||
onFocus={() => {
|
||||
oldLabel.current = label ?? '';
|
||||
}}
|
||||
onChange={(e) => variable.setState({ label: e.currentTarget.value })}
|
||||
onBlur={(e) => {
|
||||
const labelUnchanged = oldLabel.current === e.currentTarget.value;
|
||||
const shouldSkip = labelUnchanged || undoRedoWasClicked(e);
|
||||
|
||||
if (shouldSkip) {
|
||||
return;
|
||||
}
|
||||
|
||||
dashboardEditActions.changeVariableLabel({
|
||||
source: variable,
|
||||
oldValue: oldLabel.current,
|
||||
newValue: e.currentTarget.value,
|
||||
});
|
||||
}}
|
||||
data-testid={selectors.components.PanelEditor.ElementEditPane.variableLabelInput}
|
||||
/>
|
||||
);
|
||||
|
@ -171,13 +203,31 @@ function VariableLabelInput({ variable }: VariableInputProps) {
|
|||
|
||||
function VariableDescriptionTextArea({ variable }: VariableInputProps) {
|
||||
const { description } = variable.useState();
|
||||
const oldDescription = useRef(description ?? '');
|
||||
|
||||
return (
|
||||
<TextArea
|
||||
id="description-text-area"
|
||||
value={description ?? ''}
|
||||
placeholder={t('dashboard.edit-pane.variable.description-placeholder', 'Descriptive text')}
|
||||
onFocus={() => {
|
||||
oldDescription.current = description ?? '';
|
||||
}}
|
||||
onChange={(e) => variable.setState({ description: e.currentTarget.value })}
|
||||
onBlur={(e) => {
|
||||
const labelUnchanged = oldDescription.current === e.currentTarget.value;
|
||||
const shouldSkip = labelUnchanged || undoRedoWasClicked(e);
|
||||
|
||||
if (shouldSkip) {
|
||||
return;
|
||||
}
|
||||
|
||||
dashboardEditActions.changeVariableDescription({
|
||||
source: variable,
|
||||
oldValue: oldDescription.current,
|
||||
newValue: e.currentTarget.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -186,7 +236,11 @@ function VariableHideInput({ variable }: VariableInputProps) {
|
|||
const { hide = VariableHide.dontHide } = variable.useState();
|
||||
|
||||
const onChange = (option: VariableHide) => {
|
||||
variable.setState({ hide: option });
|
||||
dashboardEditActions.changeVariableHideValue({
|
||||
source: variable,
|
||||
oldValue: hide,
|
||||
newValue: option,
|
||||
});
|
||||
};
|
||||
|
||||
return <VariableHideSelect hide={hide} type={variable.state.type} onChange={onChange} />;
|
||||
|
|
|
@ -5355,6 +5355,20 @@
|
|||
"tags-expected-array": "tags expected array",
|
||||
"tags-expected-strings": "tags expected array of strings"
|
||||
},
|
||||
"variable": {
|
||||
"description": {
|
||||
"action": "Change variable description"
|
||||
},
|
||||
"hide": {
|
||||
"action": "Change variable hide option"
|
||||
},
|
||||
"label": {
|
||||
"action": "Change variable label"
|
||||
},
|
||||
"name": {
|
||||
"action": "Change variable name"
|
||||
}
|
||||
},
|
||||
"version-history-comparison": {
|
||||
"button-restore": "Restore to version {{version}}",
|
||||
"label-view-json-diff": "View JSON diff",
|
||||
|
|
Loading…
Reference in New Issue