mirror of https://github.com/grafana/grafana
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 part2sanitize-queries
parent
c36f7aa92b
commit
536edee7bf
|
@ -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<AlertQuery<ExpressionQuery>>; |
||||
dispatch: Dispatch<UnknownAction>; |
||||
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<string>) => { |
||||
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<EvalFunction>) => { |
||||
// 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<HTMLInputElement>, 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 ( |
||||
<div className={styles.condition.wrapper}> |
||||
<Stack direction="column" gap={0} width="100%"> |
||||
<header className={styles.condition.header}> |
||||
<Text variant="body"> |
||||
<Trans i18nKey="alerting.simpleCondition.alertCondition">Alert condition</Trans> |
||||
</Text> |
||||
</header> |
||||
<InlineFieldRow> |
||||
<InlineField label="WHEN"> |
||||
<Select |
||||
options={reducerTypes} |
||||
value={reducerTypes.find((o) => o.value === simpleCondition.whenField)} |
||||
onChange={onReducerTypeChange} |
||||
width={20} |
||||
/> |
||||
</InlineField> |
||||
<InlineField label="OF QUERY"> |
||||
<Stack direction="row" gap={1} alignItems="center"> |
||||
<ButtonSelect options={thresholdFunctions} onChange={onEvalFunctionChange} value={thresholdFunction} /> |
||||
{isRange ? ( |
||||
<> |
||||
<Input |
||||
type="number" |
||||
width={10} |
||||
value={simpleCondition.evaluator.params[0]} |
||||
onChange={(event) => onEvaluateValueChange(event, 0)} |
||||
/> |
||||
<div> |
||||
<Trans i18nKey="alerting.simpleCondition.ofQuery.To">TO</Trans> |
||||
</div> |
||||
<Input |
||||
type="number" |
||||
width={10} |
||||
value={simpleCondition.evaluator.params[1]} |
||||
onChange={(event) => onEvaluateValueChange(event, 1)} |
||||
/> |
||||
</> |
||||
) : ( |
||||
<Input |
||||
type="number" |
||||
width={10} |
||||
onChange={onEvaluateValueChange} |
||||
value={simpleCondition.evaluator.params[0] || 0} |
||||
/> |
||||
)} |
||||
</Stack> |
||||
</InlineField> |
||||
</InlineFieldRow> |
||||
{previewData?.series && <ExpressionResult series={previewData?.series} isAlertCondition={true} />} |
||||
</Stack> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
function updateReduceExpression( |
||||
reducer: string, |
||||
expressionQueriesList: Array<AlertQuery<ExpressionQuery>>, |
||||
dispatch: Dispatch<UnknownAction> |
||||
) { |
||||
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<AlertQuery<ExpressionQuery>>, |
||||
dispatch: Dispatch<UnknownAction> |
||||
) { |
||||
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<AlertQuery<ExpressionQuery>>, |
||||
dispatch: Dispatch<UnknownAction> |
||||
) { |
||||
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<AlertQuery<ExpressionQuery>>): 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, |
||||
}), |
||||
}, |
||||
}); |
@ -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<AlertDataQuery | ExpressionQuery> = { |
||||
refId: SIMPLE_CONDITION_QUERY_ID, |
||||
datasourceUid: 'abc123', |
||||
queryType: '', |
||||
model: { refId: SIMPLE_CONDITION_QUERY_ID }, |
||||
}; |
||||
|
||||
const reduceExpression: AlertQuery<ExpressionQuery> = { |
||||
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<ExpressionQuery> = { |
||||
refId: SIMPLE_CONDITION_THRESHOLD_ID, |
||||
queryType: 'expression', |
||||
datasourceUid: '__expr__', |
||||
model: { |
||||
type: ExpressionQueryType.threshold, |
||||
refId: SIMPLE_CONDITION_THRESHOLD_ID, |
||||
}, |
||||
}; |
||||
|
||||
const expressionQueries: Array<AlertQuery<ExpressionQuery>> = [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<AlertQuery<AlertDataQuery | ExpressionQuery>> = [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<AlertQuery<AlertDataQuery | ExpressionQuery>> = [ |
||||
{ 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<AlertQuery<AlertDataQuery | ExpressionQuery>> = [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<AlertQuery<AlertDataQuery | ExpressionQuery>> = [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<AlertQuery<AlertDataQuery | ExpressionQuery>> = [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<AlertQuery<AlertDataQuery | ExpressionQuery>> = [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<AlertQuery<AlertDataQuery | ExpressionQuery>> = [dataQuery]; |
||||
const result = areQueriesTransformableToSimpleCondition(dataQueries, expressionQueries); |
||||
expect(result).toBe(true); |
||||
}); |
||||
}); |
Loading…
Reference in new issue