diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRendering.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRendering.tsx index 4d55174d175..ca0183e0e37 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRendering.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRendering.tsx @@ -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 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({ 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); + } + }, + }); + }} /> diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingData.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingData.tsx index b881486cc01..b39090bb1df 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingData.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingData.tsx @@ -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; @@ -32,7 +34,7 @@ export class ConditionalRenderingData extends ConditionalRenderingBase 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 }), + }); + }} /> ); } diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroup.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroup.tsx index 0e18d5e5e0a..ee949871a8f 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroup.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroup.tsx @@ -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 { visibility: GroupConditionVisibility; @@ -63,7 +70,7 @@ export class ConditionalRenderingGroup extends ConditionalRenderingBase 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) { const { condition, visibility, value } = model.useState(); const { variables } = sceneGraph.getVariables(model).useState(); + const itemType = useMemo(() => model.getItemType(), [model]); return ( 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 && ( - model.changeCondition(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())} 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(), + }); + }} /> ); diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupAdd.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupAdd.tsx index 110f7a0d14e..e423870f6c8 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupAdd.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupAdd.tsx @@ -9,7 +9,7 @@ import { GroupConditionItemType, ItemsWithConditionalRendering } from './types'; interface Props { itemType: ItemsWithConditionalRendering; hasVariables: boolean; - onAdd: (itemType: GroupConditionItemType) => void; + onAdd: (option: SelectableValue) => 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)} /> ); }; diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupVisibility.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupVisibility.tsx index 3ed91b28c37..0c105d798c5 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupVisibility.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupVisibility.tsx @@ -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 ( diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingTimeRangeSize.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingTimeRangeSize.tsx index 4e57cd08347..28033cf18bb 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingTimeRangeSize.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingTimeRangeSize.tsx @@ -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; @@ -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 ( model.setStateAndNotify({ value })} + onCreateOption={(value) => handleChange(value)} value={value} options={options} - onChange={({ value }) => model.setStateAndNotify({ value })} + onChange={({ value }) => handleChange(value)} /> ); diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingVariable.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingVariable.tsx index c0d1454ba8f..5045f76d885 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingVariable.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingVariable.tsx @@ -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; @@ -31,7 +34,7 @@ export class ConditionalRenderingVariable extends ConditionalRenderingBase @@ -118,7 +123,14 @@ function ConditionalRenderingVariableRenderer({ model }: SceneComponentProps 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 } }), + }); + }} /> @@ -127,13 +139,28 @@ function ConditionalRenderingVariableRenderer({ model }: SceneComponentProps 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 } }), + }); + }} /> 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 } }), + }); + }} /> ); diff --git a/public/app/features/dashboard-scene/conditional-rendering/utils.ts b/public/app/features/dashboard-scene/conditional-rendering/utils.ts index 87c71e723b9..d8e1b841692 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/utils.ts +++ b/public/app/features/dashboard-scene/conditional-rendering/utils.ts @@ -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; +}; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index a6d6f11aeb0..cbe8fac0f9c 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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?",