DashboardEdit: Undo/redo for conditional rendering (#107235)
* add undo/redo for group and base actions * add undo/redo for rules * adjust circular dependency issue * add itemType translations * adjust how we get rendering group
This commit is contained in:
@@ -7,7 +7,7 @@ import { ConditionalRenderingChangedEvent } from '../edit-pane/shared';
|
||||
import { ConditionalRenderingBase } from './ConditionalRenderingBase';
|
||||
import { ConditionalRenderingGroup } from './ConditionalRenderingGroup';
|
||||
import { ItemsWithConditionalRendering } from './types';
|
||||
import { getItemType } from './utils';
|
||||
import { getItemType, translatedItemType } from './utils';
|
||||
|
||||
export interface ConditionalRenderingState extends SceneObjectState {
|
||||
rootGroup: ConditionalRenderingGroup;
|
||||
@@ -20,7 +20,7 @@ export class ConditionalRendering extends SceneObjectBase<ConditionalRenderingSt
|
||||
return t(
|
||||
'dashboard.conditional-rendering.root.info',
|
||||
'Set rules to control {{type}} visibility by matching any or all rules.',
|
||||
{ type: this.getItemType() }
|
||||
{ type: translatedItemType(this.getItemType()) }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+39
-2
@@ -4,8 +4,16 @@ import { Trans, t } from '@grafana/i18n';
|
||||
import { SceneComponentProps, sceneGraph, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
|
||||
import { Alert, Icon, IconButton, Stack, Text, Tooltip } from '@grafana/ui';
|
||||
|
||||
import { dashboardEditActions } from '../edit-pane/shared';
|
||||
|
||||
import { ConditionalRendering } from './ConditionalRendering';
|
||||
import { ConditionalRenderingKindTypes, ConditionValues, ItemsWithConditionalRendering } from './types';
|
||||
import { ConditionalRenderingGroup } from './ConditionalRenderingGroup';
|
||||
import {
|
||||
ConditionalRenderingConditions,
|
||||
ConditionalRenderingKindTypes,
|
||||
ConditionValues,
|
||||
ItemsWithConditionalRendering,
|
||||
} from './types';
|
||||
|
||||
export interface ConditionalRenderingBaseState<V = ConditionValues> extends SceneObjectState {
|
||||
value: V;
|
||||
@@ -74,6 +82,22 @@ export abstract class ConditionalRenderingBase<
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
public findRule() {
|
||||
return this.getRenderingGroup().getRule(this.state.key!);
|
||||
}
|
||||
|
||||
public undoDeleted(index: number, rule: ConditionalRenderingConditions) {
|
||||
const group = this.getRenderingGroup();
|
||||
const restoredState = [...group.state.value];
|
||||
restoredState.splice(index, 0, rule);
|
||||
group.setStateAndNotify({ value: restoredState });
|
||||
}
|
||||
|
||||
private getRenderingGroup(): ConditionalRenderingGroup {
|
||||
// TODO: Adjust once nested rules are introduced to get relevant ConditionalRenderingGroup
|
||||
return this._getConditionalLogicRoot().state.rootGroup;
|
||||
}
|
||||
|
||||
private _getConditionalLogicRoot(): ConditionalRendering {
|
||||
return sceneGraph.getAncestor(this, ConditionalRendering);
|
||||
}
|
||||
@@ -115,7 +139,20 @@ function ConditionalRenderingBaseRenderer<T extends ConditionalRenderingBase>({
|
||||
<IconButton
|
||||
aria-label={t('dashboard.conditional-rendering.conditions.shared.delete-condition', 'Delete Condition')}
|
||||
name="trash-alt"
|
||||
onClick={() => model.onDelete()}
|
||||
onClick={() => {
|
||||
const { rule, ruleIndex } = model.findRule();
|
||||
|
||||
dashboardEditActions.edit({
|
||||
description: t('dashboard.conditional-rendering.conditions.shared.delete-condition', 'Delete Condition'),
|
||||
source: model,
|
||||
perform: () => model.onDelete(),
|
||||
undo: () => {
|
||||
if (rule) {
|
||||
model.undoDeleted(ruleIndex, rule);
|
||||
}
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
+11
-2
@@ -6,10 +6,12 @@ import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
|
||||
import { ConditionalRenderingDataKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
|
||||
import { Combobox, ComboboxOption } from '@grafana/ui';
|
||||
|
||||
import { dashboardEditActions } from '../edit-pane/shared';
|
||||
import { AutoGridItem } from '../scene/layout-auto-grid/AutoGridItem';
|
||||
|
||||
import { ConditionalRenderingBase, ConditionalRenderingBaseState } from './ConditionalRenderingBase';
|
||||
import { ConditionalRenderingSerializerRegistryItem, DataConditionValue, ItemsWithConditionalRendering } from './types';
|
||||
import { translatedItemType } from './utils';
|
||||
|
||||
type ConditionalRenderingDataState = ConditionalRenderingBaseState<DataConditionValue>;
|
||||
|
||||
@@ -32,7 +34,7 @@ export class ConditionalRenderingData extends ConditionalRenderingBase<Condition
|
||||
return t(
|
||||
'dashboard.conditional-rendering.conditions.data.info',
|
||||
'Show or hide the {{type}} based on query results.',
|
||||
{ type: this.getItemType() }
|
||||
{ type: translatedItemType(this.getItemType()) }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -126,7 +128,14 @@ function ConditionalRenderingDataRenderer({ model }: SceneComponentProps<Conditi
|
||||
<Combobox
|
||||
options={enableConditionOptions}
|
||||
value={enableConditionOption}
|
||||
onChange={({ value }) => model.setStateAndNotify({ value: Boolean(value) })}
|
||||
onChange={({ value: val }) => {
|
||||
dashboardEditActions.edit({
|
||||
description: t('dashboard.edit-actions.edit-query-result-rule', 'Change query result rule'),
|
||||
source: model,
|
||||
perform: () => model.setStateAndNotify({ value: Boolean(val) }),
|
||||
undo: () => model.setStateAndNotify({ value }),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+59
-6
@@ -1,8 +1,13 @@
|
||||
import { capitalize, lowerCase } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
|
||||
import { ConditionalRenderingGroupKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
|
||||
import { Stack } from '@grafana/ui';
|
||||
|
||||
import { dashboardEditActions } from '../edit-pane/shared';
|
||||
|
||||
import { ConditionalRenderingBase, ConditionalRenderingBaseState } from './ConditionalRenderingBase';
|
||||
import { ConditionalRenderingData } from './ConditionalRenderingData';
|
||||
import { ConditionalRenderingGroupAdd } from './ConditionalRenderingGroupAdd';
|
||||
@@ -18,7 +23,9 @@ import {
|
||||
GroupConditionItemType,
|
||||
GroupConditionVisibility,
|
||||
GroupConditionValue,
|
||||
ConditionalRenderingConditions,
|
||||
} from './types';
|
||||
import { translatedItemType } from './utils';
|
||||
|
||||
export interface ConditionalRenderingGroupState extends ConditionalRenderingBaseState<GroupConditionValue> {
|
||||
visibility: GroupConditionVisibility;
|
||||
@@ -63,7 +70,7 @@ export class ConditionalRenderingGroup extends ConditionalRenderingBase<Conditio
|
||||
this.setStateAndNotify({ condition });
|
||||
}
|
||||
|
||||
public addItem(itemType: GroupConditionItemType) {
|
||||
public createItem(itemType: GroupConditionItemType) {
|
||||
const item =
|
||||
itemType === 'data'
|
||||
? ConditionalRenderingData.createEmpty()
|
||||
@@ -71,6 +78,10 @@ export class ConditionalRenderingGroup extends ConditionalRenderingBase<Conditio
|
||||
? ConditionalRenderingVariable.createEmpty(sceneGraph.getVariables(this).state.variables[0].state.name)
|
||||
: ConditionalRenderingTimeRangeSize.createEmpty();
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public addItem(item: ConditionalRenderingConditions) {
|
||||
// We don't use `setStateAndNotify` here because
|
||||
// We need to set a parent and activate the new condition before notifying the root
|
||||
this.setState({ value: [...this.state.value, item] });
|
||||
@@ -86,6 +97,18 @@ export class ConditionalRenderingGroup extends ConditionalRenderingBase<Conditio
|
||||
this.setStateAndNotify({ value: this.state.value.filter((condition) => condition.state.key !== key) });
|
||||
}
|
||||
|
||||
public removeLastItem() {
|
||||
const newValues = [...this.state.value];
|
||||
newValues.pop();
|
||||
this.setStateAndNotify({ value: newValues });
|
||||
}
|
||||
|
||||
public getRule(key: string) {
|
||||
const ruleIndex = this.state.value.findIndex((rule) => rule.state.key === key);
|
||||
const rule = this.state.value[ruleIndex];
|
||||
return { rule, ruleIndex };
|
||||
}
|
||||
|
||||
public serialize(): ConditionalRenderingGroupKind {
|
||||
if (this.state.value.some((item) => item instanceof ConditionalRenderingGroup)) {
|
||||
throw new Error('ConditionalRenderingGroup cannot contain nested ConditionalRenderingGroups');
|
||||
@@ -127,22 +150,52 @@ export class ConditionalRenderingGroup extends ConditionalRenderingBase<Conditio
|
||||
function ConditionalRenderingGroupRenderer({ model }: SceneComponentProps<ConditionalRenderingGroup>) {
|
||||
const { condition, visibility, value } = model.useState();
|
||||
const { variables } = sceneGraph.getVariables(model).useState();
|
||||
const itemType = useMemo(() => model.getItemType(), [model]);
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={2}>
|
||||
<ConditionalRenderingGroupVisibility
|
||||
itemType={model.getItemType()}
|
||||
itemType={itemType}
|
||||
value={visibility}
|
||||
onChange={(value) => model.changeVisibility(value)}
|
||||
onChange={(value) => {
|
||||
dashboardEditActions.edit({
|
||||
description: t('dashboard.conditional-rendering.conditions.group.visibility.label', '{{type}} visibility', {
|
||||
type: capitalize(translatedItemType(itemType)),
|
||||
}),
|
||||
source: model,
|
||||
perform: () => model.changeVisibility(value),
|
||||
undo: () => model.changeVisibility(visibility),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{value.length > 1 && (
|
||||
<ConditionalRenderingGroupCondition value={condition} onChange={(value) => model.changeCondition(value)} />
|
||||
<ConditionalRenderingGroupCondition
|
||||
value={condition}
|
||||
onChange={(value) => {
|
||||
dashboardEditActions.edit({
|
||||
description: t('dashboard.conditional-rendering.conditions.group.condition.label', 'Match rules'),
|
||||
source: model,
|
||||
perform: () => model.changeCondition(value),
|
||||
undo: () => model.changeCondition(condition),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{value.map((entry) => entry.render())}
|
||||
<ConditionalRenderingGroupAdd
|
||||
itemType={model.getItemType()}
|
||||
itemType={itemType}
|
||||
hasVariables={variables.length > 0}
|
||||
onAdd={(itemType) => model.addItem(itemType)}
|
||||
onAdd={({ value, label }) => {
|
||||
const item = model.createItem(value!);
|
||||
dashboardEditActions.edit({
|
||||
description: t('dashboard.edit-actions.add-conditional-rule', 'Add {{ruleDescription}} rule', {
|
||||
ruleDescription: lowerCase(label),
|
||||
}),
|
||||
source: model,
|
||||
perform: () => model.addItem(item),
|
||||
undo: () => model.removeLastItem(),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ import { GroupConditionItemType, ItemsWithConditionalRendering } from './types';
|
||||
interface Props {
|
||||
itemType: ItemsWithConditionalRendering;
|
||||
hasVariables: boolean;
|
||||
onAdd: (itemType: GroupConditionItemType) => void;
|
||||
onAdd: (option: SelectableValue<GroupConditionItemType>) => void;
|
||||
}
|
||||
|
||||
export const ConditionalRenderingGroupAdd = ({ itemType, hasVariables, onAdd }: Props) => {
|
||||
@@ -42,7 +42,7 @@ export const ConditionalRenderingGroupAdd = ({ itemType, hasVariables, onAdd }:
|
||||
variant="secondary"
|
||||
label={t('dashboard.conditional-rendering.conditions.group.add.button', 'Add rule')}
|
||||
options={options}
|
||||
onChange={({ value }) => onAdd(value!)}
|
||||
onChange={(option) => onAdd(option)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ import { t } from '@grafana/i18n';
|
||||
import { Field, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { GroupConditionVisibility, ItemsWithConditionalRendering } from './types';
|
||||
import { translatedItemType } from './utils';
|
||||
|
||||
interface Props {
|
||||
itemType: ItemsWithConditionalRendering;
|
||||
@@ -28,7 +29,7 @@ export const ConditionalRenderingGroupVisibility = ({ itemType, value, onChange
|
||||
return (
|
||||
<Field
|
||||
label={t('dashboard.conditional-rendering.conditions.group.visibility.label', '{{type}} visibility', {
|
||||
type: capitalize(itemType),
|
||||
type: capitalize(translatedItemType(itemType)),
|
||||
})}
|
||||
className={styles.container}
|
||||
>
|
||||
|
||||
+19
-4
@@ -1,5 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { rangeUtil, SelectableValue } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
@@ -7,8 +7,11 @@ import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
|
||||
import { ConditionalRenderingTimeRangeSizeKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
|
||||
import { Field, Select, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { dashboardEditActions } from '../edit-pane/shared';
|
||||
|
||||
import { ConditionalRenderingBase, ConditionalRenderingBaseState } from './ConditionalRenderingBase';
|
||||
import { ConditionalRenderingSerializerRegistryItem, TimeRangeSizeConditionValue } from './types';
|
||||
import { translatedItemType } from './utils';
|
||||
|
||||
type ConditionalRenderingTimeRangeSizeState = ConditionalRenderingBaseState<TimeRangeSizeConditionValue>;
|
||||
|
||||
@@ -29,7 +32,7 @@ export class ConditionalRenderingTimeRangeSize extends ConditionalRenderingBase<
|
||||
return t(
|
||||
'dashboard.conditional-rendering.conditions.time-range-size.info',
|
||||
'Show or hide the {{type}} if the dashboard time range is shorter than the selected time frame.',
|
||||
{ type: this.getItemType() }
|
||||
{ type: translatedItemType(this.getItemType()) }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -192,6 +195,18 @@ function ConditionalRenderingTimeRangeSizeRenderer({ model }: SceneComponentProp
|
||||
return [{ label: value, value }, ...staticOptions];
|
||||
}, [staticOptions, value]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newValue: string | undefined) => {
|
||||
dashboardEditActions.edit({
|
||||
description: t('dashboard.edit-actions.edit-time-range-rule', 'Change time range rule'),
|
||||
source: model,
|
||||
perform: () => model.setStateAndNotify({ value: newValue }),
|
||||
undo: () => model.setStateAndNotify({ value }),
|
||||
});
|
||||
},
|
||||
[model, value]
|
||||
);
|
||||
|
||||
return (
|
||||
<Field
|
||||
invalid={!isValid}
|
||||
@@ -201,10 +216,10 @@ function ConditionalRenderingTimeRangeSizeRenderer({ model }: SceneComponentProp
|
||||
<Select
|
||||
isClearable={false}
|
||||
allowCustomValue
|
||||
onCreateOption={(value) => model.setStateAndNotify({ value })}
|
||||
onCreateOption={(value) => handleChange(value)}
|
||||
value={value}
|
||||
options={options}
|
||||
onChange={({ value }) => model.setStateAndNotify({ value })}
|
||||
onChange={({ value }) => handleChange(value)}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
|
||||
+31
-4
@@ -5,12 +5,15 @@ import { SceneComponentProps, sceneGraph, VariableDependencyConfig } from '@graf
|
||||
import { ConditionalRenderingVariableKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
|
||||
import { Box, Combobox, ComboboxOption, Input, Stack } from '@grafana/ui';
|
||||
|
||||
import { dashboardEditActions } from '../edit-pane/shared';
|
||||
|
||||
import { ConditionalRenderingBase, ConditionalRenderingBaseState } from './ConditionalRenderingBase';
|
||||
import {
|
||||
ConditionalRenderingSerializerRegistryItem,
|
||||
VariableConditionValue,
|
||||
VariableConditionValueOperator,
|
||||
} from './types';
|
||||
import { translatedItemType } from './utils';
|
||||
|
||||
type ConditionalRenderingVariableState = ConditionalRenderingBaseState<VariableConditionValue>;
|
||||
|
||||
@@ -31,7 +34,7 @@ export class ConditionalRenderingVariable extends ConditionalRenderingBase<Condi
|
||||
return t(
|
||||
'dashboard.conditional-rendering.conditions.variable.info',
|
||||
'Show or hide the {{type}} dynamically based on the variable value.',
|
||||
{ type: this.getItemType() }
|
||||
{ type: translatedItemType(this.getItemType()) }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -110,6 +113,8 @@ function ConditionalRenderingVariableRenderer({ model }: SceneComponentProps<Con
|
||||
[]
|
||||
);
|
||||
|
||||
const undoText = t('dashboard.edit-actions.edit-template-variable-rule', 'Change template variable rule');
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={0.5}>
|
||||
<Stack direction="row" gap={0.5} grow={1}>
|
||||
@@ -118,7 +123,14 @@ function ConditionalRenderingVariableRenderer({ model }: SceneComponentProps<Con
|
||||
placeholder={t('dashboard.conditional-rendering.conditions.variable.name', 'Name')}
|
||||
options={variableNames}
|
||||
value={value.name}
|
||||
onChange={(option) => model.setStateAndNotify({ value: { ...value, name: option.value } })}
|
||||
onChange={(option) => {
|
||||
dashboardEditActions.edit({
|
||||
description: undoText,
|
||||
source: model,
|
||||
perform: () => model.setStateAndNotify({ value: { ...value, name: option.value } }),
|
||||
undo: () => model.setStateAndNotify({ value: { ...value, name: value.name } }),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -127,13 +139,28 @@ function ConditionalRenderingVariableRenderer({ model }: SceneComponentProps<Con
|
||||
minWidth={10}
|
||||
options={operatorOptions}
|
||||
value={value.operator}
|
||||
onChange={(option) => model.setStateAndNotify({ value: { ...value, operator: option.value } })}
|
||||
onChange={(option) => {
|
||||
dashboardEditActions.edit({
|
||||
description: undoText,
|
||||
source: model,
|
||||
perform: () => model.setStateAndNotify({ value: { ...value, operator: option.value } }),
|
||||
undo: () => model.setStateAndNotify({ value: { ...value, operator: value.operator } }),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Input
|
||||
placeholder={t('dashboard.conditional-rendering.conditions.variable.value', 'Value')}
|
||||
value={value.value}
|
||||
onChange={(e) => model.setStateAndNotify({ value: { ...value, value: e.currentTarget.value } })}
|
||||
onChange={(e) => {
|
||||
const eventValue = e.currentTarget.value;
|
||||
dashboardEditActions.edit({
|
||||
description: undoText,
|
||||
source: model,
|
||||
perform: () => model.setStateAndNotify({ value: { ...value, value: eventValue } }),
|
||||
undo: () => model.setStateAndNotify({ value: { ...value, value: value.value } }),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { lowerCase } from 'lodash';
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
import { SceneObject } from '@grafana/scenes';
|
||||
|
||||
import { AutoGridItem } from '../scene/layout-auto-grid/AutoGridItem';
|
||||
@@ -17,3 +20,14 @@ export function getItemType(object: SceneObject): ItemsWithConditionalRendering
|
||||
|
||||
return 'element';
|
||||
}
|
||||
|
||||
export const translatedItemType = (item: ItemsWithConditionalRendering) => {
|
||||
const translations: { [key in ItemsWithConditionalRendering]: string } = {
|
||||
panel: lowerCase(t('dashboard.edit-pane.elements.panel', 'Panel')),
|
||||
row: lowerCase(t('dashboard.edit-pane.elements.row', 'Row')),
|
||||
tab: lowerCase(t('dashboard.edit-pane.elements.tab', 'Tab')),
|
||||
element: lowerCase(t('dashboard.edit-pane.elements.element', 'Element')),
|
||||
};
|
||||
|
||||
return translations[item] || item;
|
||||
};
|
||||
|
||||
@@ -4566,6 +4566,10 @@
|
||||
},
|
||||
"edit-actions": {
|
||||
"add": "Add {{typeName}}",
|
||||
"add-conditional-rule": "Add {{ruleDescription}} rule",
|
||||
"edit-query-result-rule": "Change query result rule",
|
||||
"edit-template-variable-rule": "Change template variable rule",
|
||||
"edit-time-range-rule": "Change time range rule",
|
||||
"move": "Move {{typeName}}",
|
||||
"panel-background": "Change panel background",
|
||||
"panel-description": "Change panel description",
|
||||
@@ -4580,6 +4584,7 @@
|
||||
"edit-pane": {
|
||||
"elements": {
|
||||
"dashboard": "Dashboard",
|
||||
"element": "Element",
|
||||
"local-variable": "Local variable",
|
||||
"multiple-elements": "Multiple elements",
|
||||
"multiple-elements-delete-text": "Are you sure you want to delete these elements?",
|
||||
|
||||
Reference in New Issue
Block a user