From 536edee7bff0e393054775b02a9362c8d49ed699 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Thu, 26 Sep 2024 08:33:14 +0200 Subject: [PATCH] Alerting: Query and expressions section simplification (#93022) * Add mode switch in Query section * Implement simple query mode : WIP * fix logic switching mode * move guard and get methodd to another folder * Add more requiremts for being transformable from advanced to not advanced mode * fix usig mode when it's not a grafana managed alert * Show warning when switching to not advanced and its not possible to convert * Add feature toggle alertingQueryAndExpressionsStepMode * fix test * add translations * address PR feedback * Use form context for sharing simplfied mode used, save in local storage and use the new fields in the api * add check to valid reducer and threshold when switching to simplified mode * Use only one expression list * fix test * move existing rule check outside storeInLocalStorageValues * add id in InlineSwitch to handle onClick on label * fix * Fix default values when editing existing rule * Update dto fields for the api request * fix snapshot * Fix recording rules to not show switch mode * remove unnecessary Boolean conversion * fix areQueriesTransformableToSimpleCondition * update text * pr review nit * pr review part2 --- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 13 + .../components/expressions/Expression.tsx | 2 +- .../components/rule-editor/QueryWrapper.tsx | 33 +- .../rule-editor/RuleEditorSection.tsx | 32 +- .../alert-rule-form/AlertRuleForm.tsx | 31 +- .../QueryAndExpressionsStep.tsx | 481 ++++++++++++------ .../SimpleCondition.tsx | 241 +++++++++ ...riesTransformableToSimpleCondition.test.ts | 121 +++++ .../query-and-alert-condition/reducer.ts | 7 + .../alerting/unified/types/rule-form.ts | 5 + .../__snapshots__/rule-form.test.ts.snap | 2 + .../alerting/unified/utils/rule-form.ts | 51 +- public/app/features/expressions/guards.ts | 19 +- .../expressions/utils/expressionTypes.ts | 14 +- .../query/components/QueryEditorRow.tsx | 11 +- public/app/types/unified-alerting-dto.ts | 7 + public/locales/en-US/grafana.json | 6 + public/locales/pseudo-LOCALE/grafana.json | 6 + 23 files changed, 904 insertions(+), 192 deletions(-) create mode 100644 public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SimpleCondition.tsx create mode 100644 public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/areQueriesTransformableToSimpleCondition.test.ts diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 0ccd6b5c223..03e430aa0d1 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -202,6 +202,7 @@ Experimental features might be changed or removed without prior notice. | `exploreLogsLimitedTimeRange` | Used in Explore Logs to limit the time range | | `homeSetupGuide` | Used in Home for users who want to return to the onboarding flow or quickly find popular config pages | | `appSidecar` | Enable the app sidecar feature that allows rendering 2 apps at the same time | +| `alertingQueryAndExpressionsStepMode` | Enables step mode for alerting queries and expressions | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index d4706619503..f1a8b19a821 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -212,6 +212,7 @@ export interface FeatureToggles { appPlatformAccessTokens?: boolean; appSidecar?: boolean; groupAttributeSync?: boolean; + alertingQueryAndExpressionsStepMode?: boolean; improvedExternalSessionHandling?: boolean; useSessionStorageForRedirection?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index cafad0f1346..733cf634261 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1459,6 +1459,13 @@ var ( Owner: identityAccessTeam, HideFromDocs: true, }, + { + Name: "alertingQueryAndExpressionsStepMode", + Description: "Enables step mode for alerting queries and expressions", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + FrontendOnly: true, + }, { Name: "improvedExternalSessionHandling", Description: "Enable improved support for external sessions in Grafana", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 8cafcf6844f..2ab59f20b61 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -193,5 +193,6 @@ homeSetupGuide,experimental,@grafana/growth-and-onboarding,false,false,true appPlatformAccessTokens,experimental,@grafana/identity-access-team,false,false,false appSidecar,experimental,@grafana/explore-squad,false,false,false groupAttributeSync,experimental,@grafana/identity-access-team,false,false,false +alertingQueryAndExpressionsStepMode,experimental,@grafana/alerting-squad,false,false,true improvedExternalSessionHandling,experimental,@grafana/identity-access-team,false,false,false useSessionStorageForRedirection,preview,@grafana/identity-access-team,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index f32b5c65dca..09aafaa2a5e 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -783,6 +783,10 @@ const ( // Enable the groupsync extension for managing Group Attribute Sync feature FlagGroupAttributeSync = "groupAttributeSync" + // FlagAlertingQueryAndExpressionsStepMode + // Enables step mode for alerting queries and expressions + FlagAlertingQueryAndExpressionsStepMode = "alertingQueryAndExpressionsStepMode" + // FlagImprovedExternalSessionHandling // Enable improved support for external sessions in Grafana FlagImprovedExternalSessionHandling = "improvedExternalSessionHandling" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 9a2acd079e6..b2bf3a33aef 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -240,6 +240,19 @@ "hideFromAdminPage": true } }, + { + "metadata": { + "name": "alertingQueryAndExpressionsStepMode", + "resourceVersion": "1725978395461", + "creationTimestamp": "2024-09-10T14:26:35Z" + }, + "spec": { + "description": "Enables step mode for alerting queries and expressions", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "frontend": true + } + }, { "metadata": { "name": "alertingQueryOptimization", diff --git a/public/app/features/alerting/unified/components/expressions/Expression.tsx b/public/app/features/alerting/unified/components/expressions/Expression.tsx index 9110dbbf8fa..20a0a9561bb 100644 --- a/public/app/features/alerting/unified/components/expressions/Expression.tsx +++ b/public/app/features/alerting/unified/components/expressions/Expression.tsx @@ -51,7 +51,7 @@ export const Expression: FC = ({ onSetCondition, onUpdateRefId, onRemoveExpression, - onUpdateExpressionType, + onUpdateExpressionType, // this method is not used? maybe we should remove it onChangeQuery, }) => { const styles = useStyles2(getStyles); diff --git a/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx b/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx index 5263fea8af6..d5468f64cde 100644 --- a/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx @@ -1,7 +1,8 @@ import { css } from '@emotion/css'; import { cloneDeep } from 'lodash'; -import { ChangeEvent, useState } from 'react'; import * as React from 'react'; +import { ChangeEvent, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; import { CoreApp, @@ -14,11 +15,12 @@ import { ThresholdsConfig, } from '@grafana/data'; import { DataQuery } from '@grafana/schema'; -import { GraphThresholdsStyleMode, Icon, InlineField, Input, Tooltip, useStyles2, Stack } from '@grafana/ui'; +import { GraphThresholdsStyleMode, Icon, InlineField, Input, Stack, Tooltip, useStyles2 } from '@grafana/ui'; import { logInfo } from 'app/features/alerting/unified/Analytics'; import { QueryEditorRow } from 'app/features/query/components/QueryEditorRow'; import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; +import { RuleFormValues } from '../../types/rule-form'; import { msToSingleUnitDuration } from '../../utils/time'; import { ExpressionStatusIndicator } from '../expressions/ExpressionStatusIndicator'; @@ -78,6 +80,9 @@ export const QueryWrapper = ({ const [dsInstance, setDsInstance] = useState(); const defaults = dsInstance?.getDefaultQuery ? dsInstance.getDefaultQuery(CoreApp.UnifiedAlerting) : {}; + const { getValues } = useFormContext(); + const isAdvancedMode = getValues('editorSettings.simplifiedQueryEditor') !== true; + const queryWithDefaults = { ...defaults, ...cloneDeep(query.model), @@ -123,7 +128,17 @@ export const QueryWrapper = ({ } // TODO add a warning label here too when the data looks like time series data and is used as an alert condition - function HeaderExtras({ query, error, index }: { query: AlertQuery; error?: Error; index: number }) { + function HeaderExtras({ + query, + error, + index, + isAdvancedMode = true, + }: { + query: AlertQuery; + error?: Error; + index: number; + isAdvancedMode?: boolean; + }) { const queryOptions: AlertQueryOptions = { maxDataPoints: query.model.maxDataPoints, minInterval: query.model.intervalMs ? msToSingleUnitDuration(query.model.intervalMs) : undefined, @@ -145,7 +160,12 @@ export const QueryWrapper = ({ onChangeQueryOptions={onChangeQueryOptions} index={index} /> - onSetCondition(query.refId)} isCondition={isAlertCondition} /> + {isAdvancedMode && ( + onSetCondition(query.refId)} + isCondition={isAlertCondition} + /> + )} ); } @@ -160,6 +180,7 @@ export const QueryWrapper = ({
alerting + hideActionButtons={!isAdvancedMode} collapsable={false} dataSource={dsSettings} onDataSourceLoaded={setDsInstance} @@ -174,7 +195,9 @@ export const QueryWrapper = ({ onAddQuery={() => onDuplicateQuery(cloneDeep(query))} onRunQuery={onRunQueries} queries={editorQueries} - renderHeaderExtras={() => } + renderHeaderExtras={() => ( + + )} app={CoreApp.UnifiedAlerting} hideHideQueryButton={true} /> diff --git a/public/app/features/alerting/unified/components/rule-editor/RuleEditorSection.tsx b/public/app/features/alerting/unified/components/rule-editor/RuleEditorSection.tsx index 200e1bc8583..3c0083fe47c 100644 --- a/public/app/features/alerting/unified/components/rule-editor/RuleEditorSection.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/RuleEditorSection.tsx @@ -1,15 +1,19 @@ import { css, cx } from '@emotion/css'; -import { ReactElement } from 'react'; import * as React from 'react'; +import { ReactElement } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { FieldSet, Text, useStyles2, Stack } from '@grafana/ui'; +import { FieldSet, InlineSwitch, Stack, Text, useStyles2 } from '@grafana/ui'; export interface RuleEditorSectionProps { title: string; stepNo: number; description?: string | ReactElement; fullWidth?: boolean; + switchMode?: { + isAdvancedMode: boolean; + setAdvancedMode: (isAdvanced: boolean) => void; + }; } export const RuleEditorSection = ({ @@ -18,17 +22,33 @@ export const RuleEditorSection = ({ children, fullWidth = false, description, + switchMode, }: React.PropsWithChildren) => { const styles = useStyles2(getStyles); - return (
- {stepNo}. {title} - + + + {stepNo}. {title} + + {switchMode && ( + + { + switchMode.setAdvancedMode(event.currentTarget.checked); + }} + label="Advanced options" + showLabel + transparent + /> + + )} + } > diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx index 22eda969175..7c010fcfc5c 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx @@ -35,13 +35,14 @@ import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; import { DEFAULT_GROUP_EVALUATION_INTERVAL, MANUAL_ROUTING_KEY, + SIMPLIFIED_QUERY_EDITOR_KEY, formValuesFromExistingRule, + formValuesToRulerGrafanaRuleDTO, + formValuesToRulerRuleDTO, getDefaultFormValues, getDefaultQueries, ignoreHiddenQueries, normalizeDefaultAnnotations, - formValuesToRulerGrafanaRuleDTO, - formValuesToRulerRuleDTO, } from '../../../utils/rule-form'; import { fromRulerRule, fromRulerRuleAndRuleGroupIdentifier, stringifyIdentifier } from '../../../utils/rule-id'; import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter'; @@ -135,15 +136,6 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { trackAlertRuleFormSaved({ formAction: existing ? 'update' : 'create', ruleType: values.type }); - // when creating a new rule, we save the manual routing setting in local storage - if (!existing) { - if (values.manualRouting) { - localStorage.setItem(MANUAL_ROUTING_KEY, 'true'); - } else { - localStorage.setItem(MANUAL_ROUTING_KEY, 'false'); - } - } - const ruleDefinition = grafanaTypeRule ? formValuesToRulerGrafanaRuleDTO(values) : formValuesToRulerRuleDTO(values); const ruleGroupIdentifier = existing @@ -153,6 +145,8 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { // @TODO what is "evaluateEvery" being used for? // @TODO move this to a hook too to make sure the logic here is tested for regressions? if (!existing) { + // when creating a new rule, we save the manual routing setting , and editorSettings.simplifiedQueryEditor to the local storage + storeInLocalStorageValues(values); await addRuleToRuleGroup.execute(ruleGroupIdentifier, ruleDefinition, values.evaluateEvery); } else { const ruleIdentifier = fromRulerRuleAndRuleGroupIdentifier(ruleGroupIdentifier, existing.rule); @@ -353,6 +347,21 @@ function formValuesFromPrefill(rule: Partial): RuleFormValues { }); } +function storeInLocalStorageValues(values: RuleFormValues) { + if (values.manualRouting) { + localStorage.setItem(MANUAL_ROUTING_KEY, 'true'); + } else { + localStorage.setItem(MANUAL_ROUTING_KEY, 'false'); + } + if (values.editorSettings) { + if (values.editorSettings.simplifiedQueryEditor) { + localStorage.setItem(SIMPLIFIED_QUERY_EDITOR_KEY, 'true'); + } else { + localStorage.setItem(SIMPLIFIED_QUERY_EDITOR_KEY, 'false'); + } + } +} + const getStyles = (theme: GrafanaTheme2) => ({ buttonSpinner: css({ marginRight: theme.spacing(1), diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx index ed2f94a7542..65ffe897e4a 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx @@ -3,15 +3,34 @@ import { cloneDeep } from 'lodash'; import { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; -import { getDefaultRelativeTimeRange, GrafanaTheme2 } from '@grafana/data'; +import { getDefaultRelativeTimeRange, GrafanaTheme2, ReducerID } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { config, getDataSourceSrv } from '@grafana/runtime'; -import { Alert, Button, Dropdown, Field, Icon, Menu, MenuItem, Stack, Tooltip, useStyles2 } from '@grafana/ui'; +import { + Alert, + Button, + ConfirmModal, + Dropdown, + Field, + Icon, + Menu, + MenuItem, + Stack, + Tooltip, + useStyles2, +} from '@grafana/ui'; import { Text } from '@grafana/ui/src/components/Text/Text'; +import { EvalFunction } from 'app/features/alerting/state/alertDef'; import { isExpressionQuery } from 'app/features/expressions/guards'; -import { ExpressionDatasourceUID, ExpressionQueryType, expressionTypes } from 'app/features/expressions/types'; +import { + ExpressionDatasourceUID, + ExpressionQuery, + ExpressionQueryType, + expressionTypes, + ReducerMode, +} from 'app/features/expressions/types'; import { useDispatch } from 'app/types'; -import { AlertQuery } from 'app/types/unified-alerting-dto'; +import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; import { useRulesSourcesWithRuler } from '../../../hooks/useRuleSourcesWithRuler'; import { fetchAllPromBuildInfoAction } from '../../../state/actions'; @@ -34,6 +53,14 @@ import { RuleEditorSection } from '../RuleEditorSection'; import { errorFromCurrentCondition, errorFromPreviewData, findRenamedDataQueryReferences, refIdExists } from '../util'; import { CloudDataSourceSelector } from './CloudDataSourceSelector'; +import { + getSimpleConditionFromExpressions, + SIMPLE_CONDITION_QUERY_ID, + SIMPLE_CONDITION_REDUCER_ID, + SIMPLE_CONDITION_THRESHOLD_ID, + SimpleCondition, + SimpleConditionEditor, +} from './SimpleCondition'; import { SmartAlertTypeDetector } from './SmartAlertTypeDetector'; import { DESCRIPTIONS } from './descriptions'; import { @@ -44,6 +71,7 @@ import { queriesAndExpressionsReducer, removeExpression, removeExpressions, + resetToSimpleCondition, rewireExpressions, setDataQueries, setRecordingRulesQueries, @@ -54,6 +82,44 @@ import { } from './reducer'; import { useAlertQueryRunner } from './useAlertQueryRunner'; +export function areQueriesTransformableToSimpleCondition( + dataQueries: Array>, + expressionQueries: Array> +) { + if (dataQueries.length !== 1) { + return false; + } + + if (expressionQueries.length !== 2) { + return false; + } + + const query = dataQueries[0]; + + if (query.refId !== SIMPLE_CONDITION_QUERY_ID) { + return false; + } + + const reduceExpressionIndex = expressionQueries.findIndex( + (query) => query.model.type === ExpressionQueryType.reduce && query.refId === SIMPLE_CONDITION_REDUCER_ID + ); + const reduceExpression = expressionQueries.at(reduceExpressionIndex); + const reduceOk = + reduceExpression && + reduceExpressionIndex === 0 && + (reduceExpression.model.settings?.mode === ReducerMode.Strict || + reduceExpression.model.settings?.mode === undefined); + + const thresholdExpressionIndex = expressionQueries.findIndex( + (query) => query.model.type === ExpressionQueryType.threshold && query.refId === SIMPLE_CONDITION_THRESHOLD_ID + ); + const thresholdExpression = expressionQueries.at(thresholdExpressionIndex); + const conditions = thresholdExpression?.model.conditions ?? []; + const thresholdOk = + thresholdExpression && thresholdExpressionIndex === 1 && conditions[0]?.unloadEvaluator === undefined; + return Boolean(reduceOk) && Boolean(thresholdOk); +} + interface Props { editingExistingRule: boolean; onDataChange: (error: string) => void; @@ -69,18 +135,59 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P } = useFormContext(); const { queryPreviewData, runQueries, cancelQueries, isPreviewLoading, clearPreviewData } = useAlertQueryRunner(); + const isSwitchModeEnabled = config.featureToggles.alertingQueryAndExpressionsStepMode ?? false; const initialState = { queries: getValues('queries'), }; const [{ queries }, dispatch] = useReducer(queriesAndExpressionsReducer, initialState); - const [type, condition, dataSourceName] = watch(['type', 'condition', 'dataSourceName']); + + // data queries only + const dataQueries = useMemo(() => { + return queries.filter((query) => !isExpressionQuery(query.model)); + }, [queries]); + + // expression queries only + const expressionQueries = useMemo(() => { + return queries.filter((query) => isExpressionQueryInAlert(query)); + }, [queries]); + + const [type, condition, dataSourceName, editorSettings] = watch([ + 'type', + 'condition', + 'dataSourceName', + 'editorSettings', + ]); + //if its a new rule, look at the local storage const isGrafanaAlertingType = isGrafanaAlertingRuleByType(type); const isRecordingRuleType = isCloudRecordingRuleByType(type); const isCloudAlertRuleType = isCloudAlertingRuleByType(type); + const isAdvancedMode = editorSettings?.simplifiedQueryEditor !== true || !isGrafanaAlertingType; + + const [showResetModeModal, setShowResetModal] = useState(false); + + const [simpleCondition, setSimpleCondition] = useState( + isGrafanaAlertingType && areQueriesTransformableToSimpleCondition(dataQueries, expressionQueries) + ? getSimpleConditionFromExpressions(expressionQueries) + : { + whenField: ReducerID.last, + evaluator: { + params: [0], + type: EvalFunction.IsAbove, + }, + } + ); + + // If we switch to simple mode we need to update the simple condition with the data in the queries reducer + useEffect(() => { + if (!isAdvancedMode && isGrafanaAlertingType) { + setSimpleCondition(getSimpleConditionFromExpressions(expressionQueries)); + } + }, [isAdvancedMode, expressionQueries, isGrafanaAlertingType]); + const dispatchReduxAction = useDispatch(); useEffect(() => { dispatchReduxAction(fetchAllPromBuildInfoAction()); @@ -95,10 +202,15 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P // Grafana Managed rules and recording rules do return; } - - runQueries(getValues('queries'), condition || (getValues('condition') ?? '')); + // we need to be sure the condition is set once we switch to simple mode + if (!isAdvancedMode) { + setValue('condition', SIMPLE_CONDITION_THRESHOLD_ID); + runQueries(getValues('queries'), SIMPLE_CONDITION_THRESHOLD_ID); + } else { + runQueries(getValues('queries'), condition || (getValues('condition') ?? '')); + } }, - [isCloudAlertRuleType, runQueries, getValues] + [isCloudAlertRuleType, runQueries, getValues, isAdvancedMode, setValue] ); // whenever we update the queries we have to update the form too @@ -108,16 +220,6 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P const noCompatibleDataSources = getDefaultOrFirstCompatibleDataSource() === undefined; - // data queries only - const dataQueries = useMemo(() => { - return queries.filter((query) => !isExpressionQuery(query.model)); - }, [queries]); - - // expression queries only - const expressionQueries = useMemo(() => { - return queries.filter((query) => isExpressionQuery(query.model)); - }, [queries]); - const emptyQueries = queries.length === 0; // apply some validations and asserts to the results of the evaluation when creating or editing @@ -368,168 +470,221 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P ]); const { sectionTitle, helpLabel, helpContent, helpLink } = DESCRIPTIONS[type ?? RuleFormType.grafana]; + if (!type) { return null; } + + const switchMode = + isGrafanaAlertingType && isSwitchModeEnabled + ? { + isAdvancedMode, + setAdvancedMode: (isAdvanced: boolean) => { + if (!isAdvanced) { + if (!areQueriesTransformableToSimpleCondition(dataQueries, expressionQueries)) { + setShowResetModal(true); + return; + } + } + setValue('editorSettings', { simplifiedQueryEditor: !isAdvanced }); + }, + } + : undefined; + return ( - - - {helpLabel} - - - - } - > - {/* This is the cloud data source selector */} - {isDataSourceManagedRuleByType(type) && ( - - )} + <> + + + {helpLabel} + + + + } + switchMode={switchMode} + > + {/* This is the cloud data source selector */} + {isDataSourceManagedRuleByType(type) && ( + + )} - {/* This is the PromQL Editor for recording rules */} - {isRecordingRuleType && dataSourceName && ( - - runQueriesPreview()} - onChangeQuery={onChangeRecordingRulesQueries} - panelData={queryPreviewData} - /> - - )} - - {/* This is the PromQL Editor for Cloud rules */} - {isCloudAlertRuleType && dataSourceName && ( - + {/* This is the PromQL Editor for recording rules */} + {isRecordingRuleType && dataSourceName && ( - { - return ( - - ); - }} - control={control} - rules={{ - required: { value: true, message: 'A valid expression is required' }, - }} + runQueriesPreview()} + onChangeQuery={onChangeRecordingRulesQueries} + panelData={queryPreviewData} /> - - - )} + )} - {/* This is the editor for Grafana managed rules and Grafana managed recording rules */} - {isGrafanaManagedRuleByType(type) && ( - - {/* Data Queries */} - runQueriesPreview()} - onChangeQueries={onChangeQueries} - onDuplicateQuery={onDuplicateQuery} - panelData={queryPreviewData} - condition={condition} - onSetCondition={handleSetCondition} - /> - - - - {/* We only show Switch for Grafana managed alerts */} - {isGrafanaAlertingType && ( + {/* This is the PromQL Editor for Cloud rules */} + {isCloudAlertRuleType && dataSourceName && ( + + + { + return ( + + ); + }} + control={control} + rules={{ + required: { value: true, message: 'A valid expression is required' }, + }} + /> + - )} - {/* Expression Queries */} - - Expressions - - Manipulate data returned from queries with math and other operations. - + )} - { - dispatch(removeExpression(refId)); - }} - onUpdateRefId={onUpdateRefId} - onUpdateExpressionType={(refId, type) => { - dispatch(updateExpressionType({ refId, type })); - }} - onUpdateQueryExpression={(model) => { - dispatch(updateExpression(model)); - }} - /> - {/* action buttons */} - - {config.expressionsEnabled && } - - {isPreviewLoading && ( - + {/* This is the editor for Grafana managed rules and Grafana managed recording rules */} + {isGrafanaManagedRuleByType(type) && ( + + {/* Data Queries */} + runQueriesPreview()} + onChangeQueries={onChangeQueries} + onDuplicateQuery={onDuplicateQuery} + panelData={queryPreviewData} + condition={condition} + onSetCondition={handleSetCondition} + /> + {isAdvancedMode && ( + + + )} - {!isPreviewLoading && ( - + {/* We only show Switch for Grafana managed alerts */} + {isGrafanaAlertingType && isAdvancedMode && ( + + )} + {/* Expression Queries */} + {isAdvancedMode && isGrafanaAlertingType && ( + <> + + Expressions + + Manipulate data returned from queries with math and other operations. + + + + { + dispatch(removeExpression(refId)); + }} + onUpdateRefId={onUpdateRefId} + onUpdateExpressionType={(refId, type) => { + dispatch(updateExpressionType({ refId, type })); + }} + onUpdateQueryExpression={(model) => { + dispatch(updateExpression(model)); + }} + /> + + )} + {/* action buttons */} + + {!isAdvancedMode && ( + + )} + + {isAdvancedMode && config.expressionsEnabled && } + + {isPreviewLoading && ( + + )} + {!isPreviewLoading && ( + + )} + + + + {/* No Queries */} + {emptyQueries && ( + + Create at least one query or expression to be alerted on + )} + )} + - {/* No Queries */} - {emptyQueries && ( - - Create at least one query or expression to be alerted on - - )} - - )} - + { + setValue('editorSettings', { simplifiedQueryEditor: true }); + setShowResetModal(false); + dispatch(resetToSimpleCondition()); + }} + onDismiss={() => setShowResetModal(false)} + /> + ); }; @@ -602,3 +757,9 @@ const useSetExpressionAndDataSource = () => { } }; }; + +function isExpressionQueryInAlert( + query: AlertQuery +): query is AlertQuery { + return isExpressionQuery(query.model); +} diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SimpleCondition.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SimpleCondition.tsx new file mode 100644 index 00000000000..6dcd0204d5f --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SimpleCondition.tsx @@ -0,0 +1,241 @@ +import { css } from '@emotion/css'; +import { produce } from 'immer'; +import { Dispatch, FormEvent } from 'react'; +import { UnknownAction } from 'redux'; + +import { GrafanaTheme2, PanelData, ReducerID, SelectableValue } from '@grafana/data'; +import { ButtonSelect, InlineField, InlineFieldRow, Input, Select, Stack, Text, useStyles2 } from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; +import { EvalFunction } from 'app/features/alerting/state/alertDef'; +import { ExpressionQuery, ExpressionQueryType, reducerTypes, thresholdFunctions } from 'app/features/expressions/types'; +import { getReducerType } from 'app/features/expressions/utils/expressionTypes'; +import { AlertQuery } from 'app/types/unified-alerting-dto'; + +import { ExpressionResult } from '../../expressions/Expression'; + +import { updateExpression } from './reducer'; + +export const SIMPLE_CONDITION_QUERY_ID = 'A'; +export const SIMPLE_CONDITION_REDUCER_ID = 'B'; +export const SIMPLE_CONDITION_THRESHOLD_ID = 'C'; + +export interface SimpleCondition { + whenField: string; + evaluator: { + params: number[]; + type: EvalFunction; + }; +} + +/** + * This is the simple condition editor if the user is in the simple mode in the query section + */ +export interface SimpleConditionEditorProps { + simpleCondition: SimpleCondition; + onChange: (condition: SimpleCondition) => void; + expressionQueriesList: Array>; + dispatch: Dispatch; + previewData?: PanelData; +} + +/** + * + * This represents the simple condition editor for the alerting query section + * The state for this simple condition is kept in the parent component + * But we have also to keep the reducer state in sync with this condition state (both kept in the parent) + */ + +export const SimpleConditionEditor = ({ + simpleCondition, + onChange, + expressionQueriesList, + dispatch, + previewData, +}: SimpleConditionEditorProps) => { + const onReducerTypeChange = (value: SelectableValue) => { + onChange({ ...simpleCondition, whenField: value.value ?? ReducerID.last }); + updateReduceExpression(value.value ?? ReducerID.last, expressionQueriesList, dispatch); + }; + + const isRange = + simpleCondition.evaluator.type === EvalFunction.IsWithinRange || + simpleCondition.evaluator.type === EvalFunction.IsOutsideRange; + + const thresholdFunction = thresholdFunctions.find((fn) => fn.value === simpleCondition.evaluator?.type); + + const onEvalFunctionChange = (value: SelectableValue) => { + // change the condition kept in the parent + onChange({ + ...simpleCondition, + evaluator: { ...simpleCondition.evaluator, type: value.value ?? EvalFunction.IsAbove }, + }); + // update the reducer state where we store the queries + updateThresholdFunction(value.value ?? EvalFunction.IsAbove, expressionQueriesList, dispatch); + }; + + const onEvaluateValueChange = (event: FormEvent, index?: number) => { + if (isRange) { + const newParams = produce(simpleCondition.evaluator.params, (draft) => { + draft[index ?? 0] = parseFloat(event.currentTarget.value); + }); + // update the condition kept in the parent + onChange({ ...simpleCondition, evaluator: { ...simpleCondition.evaluator, params: newParams } }); + // update the reducer state where we store the queries + updateThresholdValue(parseFloat(event.currentTarget.value), index ?? 0, expressionQueriesList, dispatch); + } else { + // update the condition kept in the parent + onChange({ + ...simpleCondition, + evaluator: { ...simpleCondition.evaluator, params: [parseFloat(event.currentTarget.value)] }, + }); + // update the reducer state where we store the queries + updateThresholdValue(parseFloat(event.currentTarget.value), 0, expressionQueriesList, dispatch); + } + }; + + const styles = useStyles2(getStyles); + + return ( +
+ +
+ + Alert condition + +
+ + + onEvaluateValueChange(event, 0)} + /> +
+ TO +
+ onEvaluateValueChange(event, 1)} + /> + + ) : ( + + )} +
+ + + {previewData?.series && } + +
+ ); +}; + +function updateReduceExpression( + reducer: string, + expressionQueriesList: Array>, + dispatch: Dispatch +) { + const reduceExpression = expressionQueriesList.find( + (query) => query.model.type === ExpressionQueryType.reduce && query.model.refId === SIMPLE_CONDITION_REDUCER_ID + ); + + const newReduceExpression = reduceExpression + ? produce(reduceExpression?.model, (draft) => { + if (draft && draft.conditions) { + draft.reducer = reducer; + draft.conditions[0].reducer.type = getReducerType(reducer) ?? ReducerID.last; + } + }) + : undefined; + newReduceExpression && dispatch(updateExpression(newReduceExpression)); +} + +function updateThresholdFunction( + evaluator: EvalFunction, + expressionQueriesList: Array>, + dispatch: Dispatch +) { + const thresholdExpression = expressionQueriesList.find( + (query) => query.model.type === ExpressionQueryType.threshold && query.model.refId === SIMPLE_CONDITION_THRESHOLD_ID + ); + + const newThresholdExpression = produce(thresholdExpression, (draft) => { + if (draft && draft.model.conditions) { + draft.model.conditions[0].evaluator.type = evaluator; + } + }); + newThresholdExpression && dispatch(updateExpression(newThresholdExpression.model)); +} + +function updateThresholdValue( + value: number, + index: number, + expressionQueriesList: Array>, + dispatch: Dispatch +) { + const thresholdExpression = expressionQueriesList.find( + (query) => query.model.type === ExpressionQueryType.threshold && query.model.refId === SIMPLE_CONDITION_THRESHOLD_ID + ); + + const newThresholdExpression = produce(thresholdExpression, (draft) => { + if (draft && draft.model.conditions) { + draft.model.conditions[0].evaluator.params[index] = value; + } + }); + newThresholdExpression && dispatch(updateExpression(newThresholdExpression.model)); +} + +export function getSimpleConditionFromExpressions(expressions: Array>): SimpleCondition { + const reduceExpression = expressions.find( + (query) => query.model.type === ExpressionQueryType.reduce && query.refId === SIMPLE_CONDITION_REDUCER_ID + ); + const thresholdExpression = expressions.find( + (query) => query.model.type === ExpressionQueryType.threshold && query.refId === SIMPLE_CONDITION_THRESHOLD_ID + ); + const conditionsFromThreshold = thresholdExpression?.model.conditions ?? []; + return { + whenField: reduceExpression?.model.reducer ?? ReducerID.last, + evaluator: { + params: [...conditionsFromThreshold[0]?.evaluator?.params] ?? [0], + type: conditionsFromThreshold[0]?.evaluator?.type ?? EvalFunction.IsAbove, + }, + }; +} + +const getStyles = (theme: GrafanaTheme2) => ({ + condition: { + wrapper: css({ + display: 'flex', + border: `solid 1px ${theme.colors.border.medium}`, + flex: 1, + height: 'fit-content', + borderRadius: theme.shape.radius.default, + }), + header: css({ + background: theme.colors.background.secondary, + padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`, + borderBottom: `solid 1px ${theme.colors.border.weak}`, + flex: 1, + }), + }, +}); diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/areQueriesTransformableToSimpleCondition.test.ts b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/areQueriesTransformableToSimpleCondition.test.ts new file mode 100644 index 00000000000..0357d99f4d1 --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/areQueriesTransformableToSimpleCondition.test.ts @@ -0,0 +1,121 @@ +// QueryAndExpressionsStep.test.tsx + +import { produce } from 'immer'; + +import { EvalFunction } from 'app/features/alerting/state/alertDef'; +import { ExpressionQuery, ExpressionQueryType, ReducerMode } from 'app/features/expressions/types'; +import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; + +import { areQueriesTransformableToSimpleCondition } from '../QueryAndExpressionsStep'; +import { + SIMPLE_CONDITION_QUERY_ID, + SIMPLE_CONDITION_REDUCER_ID, + SIMPLE_CONDITION_THRESHOLD_ID, +} from '../SimpleCondition'; + +const dataQuery: AlertQuery = { + refId: SIMPLE_CONDITION_QUERY_ID, + datasourceUid: 'abc123', + queryType: '', + model: { refId: SIMPLE_CONDITION_QUERY_ID }, +}; + +const reduceExpression: AlertQuery = { + refId: SIMPLE_CONDITION_REDUCER_ID, + queryType: 'expression', + datasourceUid: '__expr__', + model: { + type: ExpressionQueryType.reduce, + refId: SIMPLE_CONDITION_REDUCER_ID, + settings: { mode: ReducerMode.Strict }, + }, +}; +const thresholdExpression: AlertQuery = { + refId: SIMPLE_CONDITION_THRESHOLD_ID, + queryType: 'expression', + datasourceUid: '__expr__', + model: { + type: ExpressionQueryType.threshold, + refId: SIMPLE_CONDITION_THRESHOLD_ID, + }, +}; + +const expressionQueries: Array> = [reduceExpression, thresholdExpression]; + +describe('areQueriesTransformableToSimpleCondition', () => { + it('should return false if dataQueries length is not 1', () => { + // zero dataQueries + expect(areQueriesTransformableToSimpleCondition([], expressionQueries)).toBe(false); + // more than one dataQueries + expect(areQueriesTransformableToSimpleCondition([dataQuery, dataQuery], expressionQueries)).toBe(false); + }); + it('should return false if expressionQueries length is not 2', () => { + const dataQueries: Array> = [dataQuery]; + const result = areQueriesTransformableToSimpleCondition(dataQueries, []); + expect(result).toBe(false); + }); + + it('should return false if the dataQuery refId does not match SIMPLE_CONDITION_QUERY_ID', () => { + const dataQueries: Array> = [ + { refId: 'notSimpleCondition', datasourceUid: 'abc123', queryType: '', model: { refId: 'notSimpleCondition' } }, + ]; + const result = areQueriesTransformableToSimpleCondition(dataQueries, expressionQueries); + expect(result).toBe(false); + }); + it('should return false if no reduce expression is found with correct type and refId', () => { + const dataQueries: Array> = [dataQuery]; + const result = areQueriesTransformableToSimpleCondition(dataQueries, [ + { ...reduceExpression, refId: 'hello' }, + thresholdExpression, + ]); + expect(result).toBe(false); + }); + + it('should return false if no threshold expression is found with correct type and refId', () => { + const dataQueries: Array> = [dataQuery]; + const result = areQueriesTransformableToSimpleCondition(dataQueries, [ + reduceExpression, + { ...thresholdExpression, refId: 'hello' }, + ]); + expect(result).toBe(false); + }); + + it('should return false if reduceExpression settings mode is not ReducerMode.Strict', () => { + const dataQueries: Array> = [dataQuery]; + const transformedReduceExpression = produce(reduceExpression, (draft) => { + draft.model.settings = { mode: ReducerMode.DropNonNumbers }; + }); + + const result = areQueriesTransformableToSimpleCondition(dataQueries, [ + transformedReduceExpression, + thresholdExpression, + ]); + expect(result).toBe(false); + }); + + it('should return false if thresholdExpression unloadEvaluator has a value', () => { + const dataQueries: Array> = [dataQuery]; + + const transformedThresholdExpression = produce(thresholdExpression, (draft) => { + draft.model.conditions = [ + { + evaluator: { params: [1], type: EvalFunction.IsAbove }, + unloadEvaluator: { params: [1], type: EvalFunction.IsAbove }, + query: { params: ['A'] }, + reducer: { params: [], type: 'avg' }, + type: 'query', + }, + ]; + }); + const result = areQueriesTransformableToSimpleCondition(dataQueries, [ + reduceExpression, + transformedThresholdExpression, + ]); + expect(result).toBe(false); + }); + it('should return true when all conditions are met', () => { + const dataQueries: Array> = [dataQuery]; + const result = areQueriesTransformableToSimpleCondition(dataQueries, expressionQueries); + expect(result).toBe(true); + }); +}); diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts index 44ebf2f1f18..e1e7f66092b 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts @@ -16,6 +16,7 @@ import { AlertQuery } from 'app/types/unified-alerting-dto'; import { logError } from '../../../Analytics'; import { getDefaultOrFirstCompatibleDataSource } from '../../../utils/datasource'; +import { getDefaultQueries } from '../../../utils/rule-form'; import { createDagFromQueries, getOriginOfRefId } from '../dag'; import { queriesWithUpdatedReferences, refIdExists } from '../util'; @@ -58,6 +59,8 @@ export const updateExpressionTimeRange = createAction('updateExpressionTimeRange export const updateMaxDataPoints = createAction<{ refId: string; maxDataPoints: number }>('updateMaxDataPoints'); export const updateMinInterval = createAction<{ refId: string; minInterval: string }>('updateMinInterval'); +export const resetToSimpleCondition = createAction('resetToSimpleCondition'); + export const setRecordingRulesQueries = createAction<{ recordingRuleQueries: AlertQuery[]; expression: string }>( 'setRecordingRulesQueries' ); @@ -65,6 +68,10 @@ export const setRecordingRulesQueries = createAction<{ recordingRuleQueries: Ale export const queriesAndExpressionsReducer = createReducer(initialState, (builder) => { // data queries actions builder + // simple condition actions + .addCase(resetToSimpleCondition, (state) => { + state.queries = getDefaultQueries(); + }) .addCase(duplicateQuery, (state, { payload }) => { state.queries = addQuery(state.queries, payload); }) diff --git a/public/app/features/alerting/unified/types/rule-form.ts b/public/app/features/alerting/unified/types/rule-form.ts index 3e65e2ca556..e99d56ee7e0 100644 --- a/public/app/features/alerting/unified/types/rule-form.ts +++ b/public/app/features/alerting/unified/types/rule-form.ts @@ -25,6 +25,10 @@ export interface AlertManagerManualRouting { [key: string]: ContactPoint; } +export interface SimplifiedEditor { + simplifiedQueryEditor: boolean; +} + export interface RuleFormValues { // common name: string; @@ -46,6 +50,7 @@ export interface RuleFormValues { isPaused?: boolean; manualRouting: boolean; // if true contactPoints are used. This field will not be used for saving the rule contactPoints?: AlertManagerManualRouting; + editorSettings?: SimplifiedEditor; metric?: string; // cortex / loki rules diff --git a/public/app/features/alerting/unified/utils/__snapshots__/rule-form.test.ts.snap b/public/app/features/alerting/unified/utils/__snapshots__/rule-form.test.ts.snap index 70899d7e938..71e08950c88 100644 --- a/public/app/features/alerting/unified/utils/__snapshots__/rule-form.test.ts.snap +++ b/public/app/features/alerting/unified/utils/__snapshots__/rule-form.test.ts.snap @@ -9,6 +9,7 @@ exports[`formValuesToRulerGrafanaRuleDTO should correctly convert rule form valu "data": [], "exec_err_state": "Error", "is_paused": false, + "metadata": undefined, "no_data_state": "NoData", "notification_settings": undefined, "title": "", @@ -59,6 +60,7 @@ exports[`formValuesToRulerGrafanaRuleDTO should not save both instant and range ], "exec_err_state": "Error", "is_paused": false, + "metadata": undefined, "no_data_state": "NoData", "notification_settings": undefined, "title": "", diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index 547e3031aea..231a3c51acf 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -43,7 +43,13 @@ import { type KVObject = { key: string; value: string }; import { EvalFunction } from '../../state/alertDef'; -import { AlertManagerManualRouting, ContactPoint, RuleFormType, RuleFormValues } from '../types/rule-form'; +import { + AlertManagerManualRouting, + ContactPoint, + RuleFormType, + RuleFormValues, + SimplifiedEditor, +} from '../types/rule-form'; import { getRulesAccess } from './access-control'; import { Annotation, defaultAnnotations } from './constants'; @@ -62,6 +68,7 @@ import { formatPrometheusDuration, parseInterval, safeParsePrometheusDuration } export type PromOrLokiQuery = PromQuery | LokiQuery; export const MANUAL_ROUTING_KEY = 'grafana.alerting.manualRouting'; +export const SIMPLIFIED_QUERY_EDITOR_KEY = 'grafana.alerting.simplifiedQueryEditor'; // even if the min interval is < 1m we should default to 1m, but allow arbitrary values for minInterval > 1m const GROUP_EVALUATION_MIN_INTERVAL_MS = safeParsePrometheusDuration(config.unifiedAlerting?.minInterval ?? '10s'); @@ -98,6 +105,7 @@ export const getDefaultFormValues = (): RuleFormValues => { overrideGrouping: false, overrideTimings: false, muteTimeIntervals: [], + editorSettings: getDefaultEditorSettings(), // cortex / loki namespace: '', @@ -119,6 +127,18 @@ export const getDefautManualRouting = () => { return manualRouting !== 'false'; }; +function getDefaultEditorSettings() { + const editorSettingsEnabled = config.featureToggles.alertingQueryAndExpressionsStepMode ?? false; + if (!editorSettingsEnabled) { + return undefined; + } + //then, check in local storage if the user has saved last rule with simplified query editor + const queryEditorSettings = localStorage.getItem(SIMPLIFIED_QUERY_EDITOR_KEY); + return { + simplifiedQueryEditor: queryEditorSettings !== 'false', + }; +} + export function formValuesToRulerRuleDTO(values: RuleFormValues): RulerRuleDTO { const { name, expression, forTime, forTimeUnit, keepFiringForTime, keepFiringForTimeUnit, type } = values; @@ -202,7 +222,11 @@ export function getNotificationSettingsForDTO( } return undefined; } - +function getEditorSettingsForDTO(simplifiedEditor: SimplifiedEditor) { + return { + simplified_query_and_expressions_section: simplifiedEditor.simplifiedQueryEditor, + }; +} export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): PostableRuleGrafanaRuleDTO { const { name, @@ -222,6 +246,9 @@ export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): Postabl } const notificationSettings = getNotificationSettingsForDTO(manualRouting, contactPoints); + const metadata = values.editorSettings + ? { editor_settings: getEditorSettingsForDTO(values.editorSettings) } + : undefined; const annotations = arrayToRecord(cleanAnnotations(values.annotations)); const labels = arrayToRecord(cleanLabels(values.labels)); @@ -241,6 +268,7 @@ export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): Postabl no_data_state: noDataState, exec_err_state: execErrState, notification_settings: notificationSettings, + metadata, }, annotations, labels, @@ -307,6 +335,23 @@ export function getContactPointsFromDTO(ga: GrafanaRuleDefinition): AlertManager return routingSettings; } +function getEditorSettingsFromDTO(ga: GrafanaRuleDefinition) { + // we need to check if the feature toggle is enabled as it might be disabled after the rule was created with the feature enabled + if (!config.featureToggles.alertingQueryAndExpressionsStepMode) { + return undefined; + } + + if (ga.metadata?.editor_settings) { + return { + simplifiedQueryEditor: ga.metadata.editor_settings.simplified_query_and_expressions_section, + }; + } + + return { + simplifiedQueryEditor: false, + }; +} + export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleFormValues { const { ruleSourceName, namespace, group, rule } = ruleWithLocation; @@ -353,6 +398,8 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF contactPoints: routingSettings, manualRouting: Boolean(routingSettings), + + editorSettings: getEditorSettingsFromDTO(ga), }; } else { throw new Error('Unexpected type of rule for grafana rules source'); diff --git a/public/app/features/expressions/guards.ts b/public/app/features/expressions/guards.ts index ba272a88537..40a61add235 100644 --- a/public/app/features/expressions/guards.ts +++ b/public/app/features/expressions/guards.ts @@ -1,7 +1,7 @@ import { isExpressionReference } from '@grafana/runtime/src/utils/DataSourceWithBackend'; import { DataQuery } from '@grafana/schema'; -import { ExpressionQuery, ExpressionQueryType } from './types'; +import { ExpressionQuery, ExpressionQueryType, ReducerType } from './types'; export const isExpressionQuery = (dataQuery?: DataQuery): dataQuery is ExpressionQuery => { if (!dataQuery) { @@ -19,3 +19,20 @@ export const isExpressionQuery = (dataQuery?: DataQuery): dataQuery is Expressio } return Object.values(ExpressionQueryType).includes(expression.type); }; + +export function isReducerType(value: string): value is ReducerType { + return [ + 'avg', + 'min', + 'max', + 'sum', + 'count', + 'last', + 'median', + 'diff', + 'diff_abs', + 'percent_diff', + 'percent_diff_abs', + 'count_non_null', + ].includes(value); +} diff --git a/public/app/features/expressions/utils/expressionTypes.ts b/public/app/features/expressions/utils/expressionTypes.ts index 27c8b2c5941..514dfdcb907 100644 --- a/public/app/features/expressions/utils/expressionTypes.ts +++ b/public/app/features/expressions/utils/expressionTypes.ts @@ -1,7 +1,8 @@ import { ReducerID } from '@grafana/data'; import { EvalFunction } from '../../alerting/state/alertDef'; -import { ClassicCondition, ExpressionQuery, ExpressionQueryType } from '../types'; +import { isReducerType } from '../guards'; +import { ClassicCondition, ExpressionQuery, ExpressionQueryType, ReducerType } from '../types'; export const getDefaults = (query: ExpressionQuery) => { switch (query.type) { @@ -57,3 +58,14 @@ export const defaultCondition: ClassicCondition = { type: EvalFunction.IsAbove, }, }; + +/** + * Returns the ReducerType if the value is a valid ReducerType, otherwise undefined + * @param value string + */ +export function getReducerType(value: string): ReducerType | undefined { + if (isReducerType(value)) { + return value; + } + return undefined; +} diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index 637817cfad6..fb76109bc45 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -2,8 +2,8 @@ import classNames from 'classnames'; import { cloneDeep, filter, has, uniqBy, uniqueId } from 'lodash'; import pluralize from 'pluralize'; -import { PureComponent, ReactNode } from 'react'; import * as React from 'react'; +import { PureComponent, ReactNode } from 'react'; // Utils & Services import { @@ -35,7 +35,7 @@ import { QueryOperationRow, QueryOperationRowRenderProps, } from 'app/core/components/QueryOperationRow/QueryOperationRow'; -import { t, Trans } from 'app/core/internationalization'; +import { Trans, t } from 'app/core/internationalization'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; @@ -64,6 +64,7 @@ export interface Props { history?: Array>; eventBus?: EventBusExtended; alerting?: boolean; + hideActionButtons?: boolean; onQueryCopied?: () => void; onQueryRemoved?: () => void; onQueryToggled?: (queryStatus?: boolean | undefined) => void; @@ -527,7 +528,7 @@ export class QueryEditorRow extends PureComponent extends PureComponent
diff --git a/public/app/types/unified-alerting-dto.ts b/public/app/types/unified-alerting-dto.ts index dd69743f700..4e5418c7305 100644 --- a/public/app/types/unified-alerting-dto.ts +++ b/public/app/types/unified-alerting-dto.ts @@ -219,6 +219,10 @@ export interface GrafanaNotificationSettings { repeat_interval?: string; mute_time_intervals?: string[]; } + +export interface GrafanaEditorSettings { + simplified_query_and_expressions_section: boolean; +} export interface PostableGrafanaRuleDefinition { uid?: string; title: string; @@ -228,6 +232,9 @@ export interface PostableGrafanaRuleDefinition { data: AlertQuery[]; is_paused?: boolean; notification_settings?: GrafanaNotificationSettings; + metadata?: { + editor_settings?: GrafanaEditorSettings; + }; record?: { metric: string; from: string; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 455063f88b9..51e5a1e67d2 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -244,6 +244,12 @@ "state": "State" }, "save-query": "Save current search" + }, + "simpleCondition": { + "alertCondition": "Alert condition", + "ofQuery": { + "To": "TO" + } } }, "annotations": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index be08f9a971b..23911fd61f2 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -244,6 +244,12 @@ "state": "Ŝŧäŧę" }, "save-query": "Ŝävę čūřřęʼnŧ şęäřčĥ" + }, + "simpleCondition": { + "alertCondition": "Åľęřŧ čőʼnđįŧįőʼn", + "ofQuery": { + "To": "ŦØ" + } } }, "annotations": {