diff --git a/.betterer.results b/.betterer.results index 89f038771ec..4197643351c 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4991,12 +4991,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"] ], "public/app/features/expressions/components/Threshold.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"] + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], "public/app/features/expressions/guards.ts:5381": [ [0, 0, 0, "\'@grafana/runtime/src/utils/DataSourceWithBackend\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], diff --git a/pkg/expr/classic/evaluator.go b/pkg/expr/classic/evaluator.go index 9c1117f057f..9f38bf2136e 100644 --- a/pkg/expr/classic/evaluator.go +++ b/pkg/expr/classic/evaluator.go @@ -48,9 +48,9 @@ func (rangedEvaluator) Kind() EvaluatorKind { // an AlertEvaluator depending on evaluation operator. func newAlertEvaluator(model ConditionEvalJSON) (evaluator, error) { switch model.Type { - case "gt", "lt": + case "gt", "lt", "eq", "ne", "gte", "lte": return newThresholdEvaluator(model) - case "within_range", "outside_range": + case "within_range", "outside_range", "within_range_included", "outside_range_included": return newRangedEvaluator(model) case "no_value": return &noValueEvaluator{}, nil @@ -70,6 +70,14 @@ func (e *thresholdEvaluator) Eval(reducedValue mathexp.Number) bool { return *fv > e.Threshold case "lt": return *fv < e.Threshold + case "eq": + return *fv == e.Threshold + case "ne": + return *fv != e.Threshold + case "gte": + return *fv >= e.Threshold + case "lte": + return *fv <= e.Threshold } return false @@ -113,6 +121,10 @@ func (e *rangedEvaluator) Eval(reducedValue mathexp.Number) bool { return (e.Lower < *fv && e.Upper > *fv) || (e.Upper < *fv && e.Lower > *fv) case "outside_range": return (e.Upper < *fv && e.Lower < *fv) || (e.Upper > *fv && e.Lower > *fv) + case "within_range_included": + return (e.Lower <= *fv && e.Upper >= *fv) || (e.Upper <= *fv && e.Lower >= *fv) + case "outside_range_included": + return (e.Upper <= *fv && e.Lower <= *fv) || (e.Upper >= *fv && e.Lower >= *fv) } return false diff --git a/pkg/expr/classic/evaluator_test.go b/pkg/expr/classic/evaluator_test.go index 92aa7ad2a40..4a82b1c920c 100644 --- a/pkg/expr/classic/evaluator_test.go +++ b/pkg/expr/classic/evaluator_test.go @@ -40,6 +40,48 @@ func TestThresholdEvaluator(t *testing.T) { inputNumber: newNumber(util.Pointer(1.0)), expected: true, }, + { + name: "value 1 is eq 1: false", + evaluator: &thresholdEvaluator{"eq", 1}, + inputNumber: newNumber(util.Pointer(1.0)), + expected: true, + }, + { + name: "value 0 is eq 0: false", + evaluator: &thresholdEvaluator{"eq", 0}, + inputNumber: newNumber(util.Pointer(0.0)), + expected: true, + }, + { + name: "value 1 is eq 0: false", + evaluator: &thresholdEvaluator{"eq", 0}, + inputNumber: newNumber(util.Pointer(1.0)), + expected: false, + }, + { + name: "value 0 is eq 1: false", + evaluator: &thresholdEvaluator{"eq", 1}, + inputNumber: newNumber(util.Pointer(0.0)), + expected: false, + }, + { + name: "value 1 is ne 1: false", + evaluator: &thresholdEvaluator{"ne", 1}, + inputNumber: newNumber(util.Pointer(1.0)), + expected: false, + }, + { + name: "value 3 is gte 3: false", + evaluator: &thresholdEvaluator{"gte", 3}, + inputNumber: newNumber(util.Pointer(3.0)), + expected: true, + }, + { + name: "value 5 is lte 4: false", + evaluator: &thresholdEvaluator{"lte", 4}, + inputNumber: newNumber(util.Pointer(5.0)), + expected: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/expr/query.panel.schema.json b/pkg/expr/query.panel.schema.json index 388781100ee..4437e10b82e 100644 --- a/pkg/expr/query.panel.schema.json +++ b/pkg/expr/query.panel.schema.json @@ -712,8 +712,14 @@ "enum": [ "gt", "lt", + "eq", + "ne", + "gte", + "lte", "within_range", - "outside_range" + "outside_range", + "within_range_included", + "outside_range_included" ], "x-enum-description": {} } @@ -744,8 +750,14 @@ "enum": [ "gt", "lt", + "eq", + "ne", + "gte", + "lte", "within_range", - "outside_range" + "outside_range", + "within_range_included", + "outside_range_included" ], "x-enum-description": {} } @@ -1013,4 +1025,4 @@ }, "additionalProperties": true, "$schema": "https://json-schema.org/draft-04/schema#" -} \ No newline at end of file +} diff --git a/pkg/expr/query.request.schema.json b/pkg/expr/query.request.schema.json index aa08911fc3c..5e8c4a60371 100644 --- a/pkg/expr/query.request.schema.json +++ b/pkg/expr/query.request.schema.json @@ -754,8 +754,14 @@ "enum": [ "gt", "lt", + "eq", + "ne", + "gte", + "lte", "within_range", - "outside_range" + "outside_range", + "within_range_included", + "outside_range_included" ], "x-enum-description": {} } @@ -786,8 +792,14 @@ "enum": [ "gt", "lt", + "eq", + "ne", + "gte", + "lte", "within_range", - "outside_range" + "outside_range", + "within_range_included", + "outside_range_included" ], "x-enum-description": {} } @@ -1071,4 +1083,4 @@ }, "additionalProperties": false, "$schema": "https://json-schema.org/draft-04/schema#" -} \ No newline at end of file +} diff --git a/pkg/expr/query.types.json b/pkg/expr/query.types.json index 092abaa7393..b24de7a4319 100644 --- a/pkg/expr/query.types.json +++ b/pkg/expr/query.types.json @@ -395,8 +395,14 @@ "enum": [ "gt", "lt", + "eq", + "ne", + "gte", + "lte", "within_range", - "outside_range" + "outside_range", + "within_range_included", + "outside_range_included" ], "type": "string", "x-enum-description": {} @@ -427,8 +433,14 @@ "enum": [ "gt", "lt", + "eq", + "ne", + "gte", + "lte", "within_range", - "outside_range" + "outside_range", + "within_range_included", + "outside_range_included" ], "type": "string", "x-enum-description": {} @@ -579,4 +591,4 @@ } } ] -} \ No newline at end of file +} diff --git a/pkg/expr/threshold.go b/pkg/expr/threshold.go index 127de9ce8a7..d07a5d7676e 100644 --- a/pkg/expr/threshold.go +++ b/pkg/expr/threshold.go @@ -32,18 +32,30 @@ type ThresholdCommand struct { type ThresholdType string const ( - ThresholdIsAbove ThresholdType = "gt" - ThresholdIsBelow ThresholdType = "lt" - ThresholdIsWithinRange ThresholdType = "within_range" - ThresholdIsOutsideRange ThresholdType = "outside_range" + ThresholdIsAbove ThresholdType = "gt" + ThresholdIsBelow ThresholdType = "lt" + ThresholdIsEqual ThresholdType = "eq" + ThresholdIsNotEqual ThresholdType = "ne" + ThresholdIsGreaterThanEqual ThresholdType = "gte" + ThresholdIsLessThanEqual ThresholdType = "lte" + ThresholdIsWithinRange ThresholdType = "within_range" + ThresholdIsOutsideRange ThresholdType = "outside_range" + ThresholdIsWithinRangeIncluded ThresholdType = "within_range_included" + ThresholdIsOutsideRangeIncluded ThresholdType = "outside_range_included" ) var ( supportedThresholdFuncs = []string{ string(ThresholdIsAbove), string(ThresholdIsBelow), + string(ThresholdIsEqual), + string(ThresholdIsNotEqual), + string(ThresholdIsGreaterThanEqual), + string(ThresholdIsLessThanEqual), string(ThresholdIsWithinRange), string(ThresholdIsOutsideRange), + string(ThresholdIsWithinRangeIncluded), + string(ThresholdIsOutsideRangeIncluded), } ) @@ -60,6 +72,16 @@ func NewThresholdCommand(refID, referenceVar string, thresholdFunc ThresholdType return nil, fmt.Errorf("incorrect number of arguments for threshold function '%s': got %d but need 2", thresholdFunc, len(conditions)) } predicate = withinRangePredicate{left: conditions[0], right: conditions[1]} + case ThresholdIsWithinRangeIncluded: + if len(conditions) < 2 { + return nil, fmt.Errorf("incorrect number of arguments for threshold function '%s': got %d but need 2", thresholdFunc, len(conditions)) + } + predicate = withinRangeIncludedPredicate{left: conditions[0], right: conditions[1]} + case ThresholdIsOutsideRangeIncluded: + if len(conditions) < 2 { + return nil, fmt.Errorf("incorrect number of arguments for threshold function '%s': got %d but need 2", thresholdFunc, len(conditions)) + } + predicate = outsideRangeIncludedPredicate{left: conditions[0], right: conditions[1]} case ThresholdIsAbove: if len(conditions) < 1 { return nil, fmt.Errorf("incorrect number of arguments for threshold function '%s': got %d but need 1", thresholdFunc, len(conditions)) @@ -70,6 +92,26 @@ func NewThresholdCommand(refID, referenceVar string, thresholdFunc ThresholdType return nil, fmt.Errorf("incorrect number of arguments for threshold function '%s': got %d but need 1", thresholdFunc, len(conditions)) } predicate = lessThanPredicate{value: conditions[0]} + case ThresholdIsEqual: + if len(conditions) < 1 { + return nil, fmt.Errorf("incorrect number of arguments for threshold function '%s': got %d but need 1", thresholdFunc, len(conditions)) + } + predicate = equalPredicate{value: conditions[0]} + case ThresholdIsNotEqual: + if len(conditions) < 1 { + return nil, fmt.Errorf("incorrect number of arguments for threshold function '%s': got %d but need 1", thresholdFunc, len(conditions)) + } + predicate = notEqualPredicate{value: conditions[0]} + case ThresholdIsGreaterThanEqual: + if len(conditions) < 1 { + return nil, fmt.Errorf("incorrect number of arguments for threshold function '%s': got %d but need 1", thresholdFunc, len(conditions)) + } + predicate = greaterThanEqualPredicate{value: conditions[0]} + case ThresholdIsLessThanEqual: + if len(conditions) < 1 { + return nil, fmt.Errorf("incorrect number of arguments for threshold function '%s': got %d but need 1", thresholdFunc, len(conditions)) + } + predicate = lessThanEqualPredicate{value: conditions[0]} default: return nil, fmt.Errorf("expected threshold function to be one of [%s], got %s", strings.Join(supportedThresholdFuncs, ", "), thresholdFunc) } @@ -279,6 +321,24 @@ func (r outsideRangePredicate) Eval(f float64) bool { return f < r.left || f > r.right } +type withinRangeIncludedPredicate struct { + left float64 + right float64 +} + +func (r withinRangeIncludedPredicate) Eval(f float64) bool { + return f >= r.left && f <= r.right +} + +type outsideRangeIncludedPredicate struct { + left float64 + right float64 +} + +func (r outsideRangeIncludedPredicate) Eval(f float64) bool { + return f <= r.left || f >= r.right +} + type lessThanPredicate struct { value float64 } @@ -294,3 +354,35 @@ type greaterThanPredicate struct { func (r greaterThanPredicate) Eval(f float64) bool { return f > r.value } + +type equalPredicate struct { + value float64 +} + +func (r equalPredicate) Eval(f float64) bool { + return f == r.value +} + +type notEqualPredicate struct { + value float64 +} + +func (r notEqualPredicate) Eval(f float64) bool { + return f != r.value +} + +type greaterThanEqualPredicate struct { + value float64 +} + +func (r greaterThanEqualPredicate) Eval(f float64) bool { + return f >= r.value +} + +type lessThanEqualPredicate struct { + value float64 +} + +func (r lessThanEqualPredicate) Eval(f float64) bool { + return f <= r.value +} diff --git a/pkg/expr/threshold_test.go b/pkg/expr/threshold_test.go index d458304ae08..2cfcadab18f 100644 --- a/pkg/expr/threshold_test.go +++ b/pkg/expr/threshold_test.go @@ -38,6 +38,26 @@ func TestNewThresholdCommand(t *testing.T) { args: []float64{0}, shouldError: false, }, + { + fn: "eq", + args: []float64{0}, + shouldError: false, + }, + { + fn: "ne", + args: []float64{0}, + shouldError: false, + }, + { + fn: "gte", + args: []float64{0}, + shouldError: false, + }, + { + fn: "lte", + args: []float64{0}, + shouldError: false, + }, { fn: "within_range", args: []float64{0, 1}, @@ -48,6 +68,16 @@ func TestNewThresholdCommand(t *testing.T) { args: []float64{0, 1}, shouldError: false, }, + { + fn: "within_range_included", + args: []float64{0, 1}, + shouldError: false, + }, + { + fn: "outside_range_included", + args: []float64{0, 1}, + shouldError: false, + }, { fn: "gt", args: []float64{}, @@ -60,6 +90,30 @@ func TestNewThresholdCommand(t *testing.T) { shouldError: true, expectedError: "incorrect number of arguments", }, + { + fn: "eq", + args: []float64{}, + shouldError: true, + expectedError: "incorrect number of arguments", + }, + { + fn: "ne", + args: []float64{}, + shouldError: true, + expectedError: "incorrect number of arguments", + }, + { + fn: "gte", + args: []float64{}, + shouldError: true, + expectedError: "incorrect number of arguments", + }, + { + fn: "lte", + args: []float64{}, + shouldError: true, + expectedError: "incorrect number of arguments", + }, { fn: "within_range", args: []float64{0}, @@ -72,6 +126,18 @@ func TestNewThresholdCommand(t *testing.T) { shouldError: true, expectedError: "incorrect number of arguments", }, + { + fn: "within_range_included", + args: []float64{0}, + shouldError: true, + expectedError: "incorrect number of arguments", + }, + { + fn: "outside_range_included", + args: []float64{0}, + shouldError: true, + expectedError: "incorrect number of arguments", + }, } for _, tc := range cases { @@ -249,6 +315,22 @@ func TestIsSupportedThresholdFunc(t *testing.T) { function: ThresholdIsBelow, supported: true, }, + { + function: ThresholdIsEqual, + supported: true, + }, + { + function: ThresholdIsNotEqual, + supported: true, + }, + { + function: ThresholdIsGreaterThanEqual, + supported: true, + }, + { + function: ThresholdIsLessThanEqual, + supported: true, + }, { function: ThresholdIsWithinRange, supported: true, @@ -257,6 +339,14 @@ func TestIsSupportedThresholdFunc(t *testing.T) { function: ThresholdIsOutsideRange, supported: true, }, + { + function: ThresholdIsWithinRangeIncluded, + supported: true, + }, + { + function: ThresholdIsOutsideRangeIncluded, + supported: true, + }, { function: "foo", supported: false, diff --git a/public/app/features/alerting/state/ThresholdMapper.ts b/public/app/features/alerting/state/ThresholdMapper.ts index 265934a61fd..48b9b7c656a 100644 --- a/public/app/features/alerting/state/ThresholdMapper.ts +++ b/public/app/features/alerting/state/ThresholdMapper.ts @@ -29,6 +29,26 @@ export class ThresholdMapper { thresholds.push({ value: value, op: 'lt', visible }); break; } + case 'eq': { + const value = evaluator.params[0]; + thresholds.push({ value: value, op: 'eq', visible }); + break; + } + case 'ne': { + const value = evaluator.params[0]; + thresholds.push({ value: value, op: 'ne', visible }); + break; + } + case 'gte': { + const value = evaluator.params[0]; + thresholds.push({ value: value, op: 'ge', visible }); + break; + } + case 'lte': { + const value = evaluator.params[0]; + thresholds.push({ value: value, op: 'le', visible }); + break; + } case 'outside_range': { const value1 = evaluator.params[0]; const value2 = evaluator.params[1]; @@ -56,6 +76,33 @@ export class ThresholdMapper { } break; } + case 'outside_range_included': { + const value1 = evaluator.params[0]; + const value2 = evaluator.params[1]; + + if (value1 >= value2) { + thresholds.push({ value: value1, op: 'ge', visible }); + thresholds.push({ value: value2, op: 'le', visible }); + } else { + thresholds.push({ value: value1, op: 'le', visible }); + thresholds.push({ value: value2, op: 'ge', visible }); + } + + break; + } + case 'within_range_included': { + const value1 = evaluator.params[0]; + const value2 = evaluator.params[1]; + + if (value1 >= value2) { + thresholds.push({ value: value1, op: 'le', visible }); + thresholds.push({ value: value2, op: 'ge', visible }); + } else { + thresholds.push({ value: value1, op: 'ge', visible }); + thresholds.push({ value: value2, op: 'le', visible }); + } + break; + } } break; } diff --git a/public/app/features/alerting/state/alertDef.ts b/public/app/features/alerting/state/alertDef.ts index 96884ce8ae1..cdcdcbda7c1 100644 --- a/public/app/features/alerting/state/alertDef.ts +++ b/public/app/features/alerting/state/alertDef.ts @@ -32,16 +32,28 @@ const alertStateSortScore = { export enum EvalFunction { 'IsAbove' = 'gt', 'IsBelow' = 'lt', + 'IsEqual' = 'eq', + 'IsNotEqual' = 'ne', + 'IsGreaterThanEqual' = 'gte', + 'IsLessThanEqual' = 'lte', 'IsOutsideRange' = 'outside_range', 'IsWithinRange' = 'within_range', + 'IsWithinRangeIncluded' = 'within_range_included', + 'IsOutsideRangeIncluded' = 'outside_range_included', 'HasNoValue' = 'no_value', } const evalFunctions = [ { value: EvalFunction.IsAbove, text: 'IS ABOVE' }, { value: EvalFunction.IsBelow, text: 'IS BELOW' }, + { value: EvalFunction.IsEqual, text: 'IS EQUAL TO' }, + { value: EvalFunction.IsNotEqual, text: 'IS NOT EQUAL TO' }, + { value: EvalFunction.IsGreaterThanEqual, text: 'IS ABOVE OR EQUAL TO' }, + { value: EvalFunction.IsLessThanEqual, text: 'IS BELOW OR EQUAL TO' }, { value: EvalFunction.IsOutsideRange, text: 'IS OUTSIDE RANGE' }, { value: EvalFunction.IsWithinRange, text: 'IS WITHIN RANGE' }, + { value: EvalFunction.IsOutsideRangeIncluded, text: 'IS OUTSIDE RANGE INCLUDED' }, + { value: EvalFunction.IsWithinRangeIncluded, text: 'IS WITHIN RANGE INCLUDED' }, { value: EvalFunction.HasNoValue, text: 'HAS NO VALUE' }, ]; diff --git a/public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx b/public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx index 1839ea4c8eb..10126744321 100644 --- a/public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx +++ b/public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx @@ -538,5 +538,10 @@ const getCommonQueryStyles = (theme: GrafanaTheme2) => ({ }); function isRangeEvaluator(evaluator: { params: number[]; type: EvalFunction }) { - return evaluator.type === EvalFunction.IsWithinRange || evaluator.type === EvalFunction.IsOutsideRange; + return ( + evaluator.type === EvalFunction.IsWithinRange || + evaluator.type === EvalFunction.IsOutsideRange || + evaluator.type === EvalFunction.IsOutsideRangeIncluded || + evaluator.type === EvalFunction.IsWithinRangeIncluded + ); } diff --git a/public/app/features/alerting/unified/components/rule-editor/util.ts b/public/app/features/alerting/unified/components/rule-editor/util.ts index 3933de4f48e..b8e20cba6b4 100644 --- a/public/app/features/alerting/unified/components/rule-editor/util.ts +++ b/public/app/features/alerting/unified/components/rule-editor/util.ts @@ -279,6 +279,53 @@ export function getThresholdsForQueries(queries: AlertQuery[], condition: string ); } + if (type === EvalFunction.IsWithinRangeIncluded) { + thresholds[refId].config.steps.push( + ...[ + { + value: -Infinity, + color: 'transparent', + }, + { + value: values[0], + color: config.theme2.colors.error.main, + }, + { + value: values[1], + color: config.theme2.colors.error.main, + }, + { + value: values[1], + color: 'transparent', + }, + ] + ); + } + + if (type === EvalFunction.IsOutsideRangeIncluded) { + thresholds[refId].config.steps.push( + ...[ + { + value: -Infinity, + color: config.theme2.colors.error.main, + }, + // we have to duplicate this value, or the graph will not display the handle in the right color + { + value: values[0], + color: config.theme2.colors.error.main, + }, + { + value: values[0], + color: 'transparent', + }, + { + value: values[1], + color: config.theme2.colors.error.main, + }, + ] + ); + } + // now also sort the threshold values, if we don't then they will look weird in the time series panel // TODO this doesn't work for negative values for now, those need to be sorted inverse thresholds[refId].config.steps.sort((a, b) => a.value - b.value); @@ -292,7 +339,10 @@ export function getThresholdsForQueries(queries: AlertQuery[], condition: string function isRangeCondition(condition: ClassicCondition) { return ( - condition.evaluator.type === EvalFunction.IsWithinRange || condition.evaluator.type === EvalFunction.IsOutsideRange + condition.evaluator.type === EvalFunction.IsWithinRange || + condition.evaluator.type === EvalFunction.IsOutsideRange || + condition.evaluator.type === EvalFunction.IsOutsideRangeIncluded || + condition.evaluator.type === EvalFunction.IsWithinRangeIncluded ); } diff --git a/public/app/features/expressions/components/Condition.tsx b/public/app/features/expressions/components/Condition.tsx index fc6455fd540..778869c5a60 100644 --- a/public/app/features/expressions/components/Condition.tsx +++ b/public/app/features/expressions/components/Condition.tsx @@ -69,7 +69,10 @@ export const Condition = ({ condition, index, onChange, onRemoveCondition, refId }); const isRange = - condition.evaluator.type === EvalFunction.IsWithinRange || condition.evaluator.type === EvalFunction.IsOutsideRange; + condition.evaluator.type === EvalFunction.IsWithinRange || + condition.evaluator.type === EvalFunction.IsOutsideRange || + condition.evaluator.type === EvalFunction.IsOutsideRangeIncluded || + condition.evaluator.type === EvalFunction.IsWithinRangeIncluded; return ( diff --git a/public/app/features/expressions/components/Threshold.tsx b/public/app/features/expressions/components/Threshold.tsx index 6474c497e16..6d0d43eb7e9 100644 --- a/public/app/features/expressions/components/Threshold.tsx +++ b/public/app/features/expressions/components/Threshold.tsx @@ -5,8 +5,9 @@ import * as React from 'react'; import { FormEvent, useEffect, useReducer } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { InlineField, InlineFieldRow, InlineSwitch, Input, Select, useStyles2, Stack } from '@grafana/ui'; +import { InlineField, InlineFieldRow, InlineSwitch, Input, Select, Stack, useStyles2 } from '@grafana/ui'; import { config } from 'app/core/config'; +import { t } from 'app/core/internationalization'; import { EvalFunction } from 'app/features/alerting/state/alertDef'; import { ClassicCondition, ExpressionQuery, thresholdFunctions } from '../types'; @@ -81,7 +82,9 @@ export const Threshold = ({ labelWidth, onChange, refIds, query, onError, useHys const isRange = conditionInState.evaluator.type === EvalFunction.IsWithinRange || - conditionInState.evaluator.type === EvalFunction.IsOutsideRange; + conditionInState.evaluator.type === EvalFunction.IsOutsideRange || + conditionInState.evaluator.type === EvalFunction.IsOutsideRangeIncluded || + conditionInState.evaluator.type === EvalFunction.IsWithinRangeIncluded; const hysteresisEnabled = Boolean(config.featureToggles?.recoveryThreshold) && useHysteresis; @@ -155,7 +158,7 @@ export const Threshold = ({ labelWidth, onChange, refIds, query, onError, useHys
- - -
- - allowOnblur.current && onUnloadValueChange(event, 0)} - defaultValue={condition.unloadEvaluator?.params[0]} - /> - -
- -
- - allowOnblur.current && onUnloadValueChange(event, 1)} - defaultValue={condition.unloadEvaluator?.params[1]} - /> - -
-
-
- - ); - } else { - return ( - - - -
- - allowOnblur.current && onUnloadValueChange(event, 0)} - defaultValue={condition.unloadEvaluator?.params[0]} - /> - -
+ switch (condition.evaluator.type) { + case EvalFunction.IsWithinRange: + if (condition.evaluator.type === EvalFunction.IsWithinRange) { + return ( + + + +
+ + allowOnblur.current && onUnloadValueChange(event, 0)} + defaultValue={condition.unloadEvaluator?.params[0]} + /> + +
+ +
+ + allowOnblur.current && onUnloadValueChange(event, 1)} + defaultValue={condition.unloadEvaluator?.params[1]} + /> + +
+
+
+
+ ); + } + case EvalFunction.IsOutsideRange: + return ( + + + +
+ + allowOnblur.current && onUnloadValueChange(event, 0)} + defaultValue={condition.unloadEvaluator?.params[0]} + /> + +
- -
- - allowOnblur.current && onUnloadValueChange(event, 1)} - defaultValue={condition.unloadEvaluator?.params[1]} - /> - -
-
-
-
- ); + +
+ + allowOnblur.current && onUnloadValueChange(event, 1)} + defaultValue={condition.unloadEvaluator?.params[1]} + /> + +
+
+
+
+ ); + case EvalFunction.IsOutsideRangeIncluded: + return ( + + + +
+ + allowOnblur.current && onUnloadValueChange(event, 0)} + defaultValue={condition.unloadEvaluator?.params[0]} + /> + +
+ +
+ + allowOnblur.current && onUnloadValueChange(event, 1)} + defaultValue={condition.unloadEvaluator?.params[1]} + /> + +
+
+
+
+ ); + case EvalFunction.IsWithinRangeIncluded: + return ( + + + +
+ + allowOnblur.current && onUnloadValueChange(event, 0)} + defaultValue={condition.unloadEvaluator?.params[0]} + /> + +
+ +
+ + allowOnblur.current && onUnloadValueChange(event, 1)} + defaultValue={condition.unloadEvaluator?.params[1]} + /> + +
+
+
+
+ ); + default: + return null; } } function RecoveryForSingleValue({ allowOnblur }: RecoveryProps) { - if (condition.evaluator.type === EvalFunction.IsAbove) { - return ( - - - { - allowOnblur.current && onUnloadValueChange(event, 0); - }} - defaultValue={condition.unloadEvaluator?.params[0]} - /> - - - ); - } else { - return ( - - - { - allowOnblur.current && onUnloadValueChange(event, 0); - }} - defaultValue={condition.unloadEvaluator?.params[0]} - /> - - - ); + switch (condition.evaluator.type) { + case EvalFunction.IsAbove: + return ( + + + { + allowOnblur.current && onUnloadValueChange(event, 0); + }} + defaultValue={condition.unloadEvaluator?.params[0]} + /> + + + ); + case EvalFunction.IsBelow: + return ( + + + { + allowOnblur.current && onUnloadValueChange(event, 0); + }} + defaultValue={condition.unloadEvaluator?.params[0]} + /> + + + ); + case EvalFunction.IsEqual: + return ( + + + { + allowOnblur.current && onUnloadValueChange(event, 0); + }} + defaultValue={condition.unloadEvaluator?.params[0]} + /> + + + ); + case EvalFunction.IsNotEqual: + return ( + + + { + allowOnblur.current && onUnloadValueChange(event, 0); + }} + defaultValue={condition.unloadEvaluator?.params[0]} + /> + + + ); + case EvalFunction.IsGreaterThanEqual: + return ( + + + { + allowOnblur.current && onUnloadValueChange(event, 0); + }} + defaultValue={condition.unloadEvaluator?.params[0]} + /> + + + ); + case EvalFunction.IsLessThanEqual: + return ( + + + { + allowOnblur.current && onUnloadValueChange(event, 0); + }} + defaultValue={condition.unloadEvaluator?.params[0]} + /> + + + ); + default: + return null; } } } diff --git a/public/app/features/expressions/components/thresholdReducer.ts b/public/app/features/expressions/components/thresholdReducer.ts index 7fa1e428a98..503baa3e727 100644 --- a/public/app/features/expressions/components/thresholdReducer.ts +++ b/public/app/features/expressions/components/thresholdReducer.ts @@ -104,12 +104,30 @@ function getUnloadEvaluatorTypeFromEvaluatorType(type: EvalFunction) { if (type === EvalFunction.IsBelow) { return EvalFunction.IsAbove; } + if (type === EvalFunction.IsEqual) { + return EvalFunction.IsNotEqual; + } + if (type === EvalFunction.IsNotEqual) { + return EvalFunction.IsEqual; + } + if (type === EvalFunction.IsGreaterThanEqual) { + return EvalFunction.IsLessThanEqual; + } + if (type === EvalFunction.IsLessThanEqual) { + return EvalFunction.IsGreaterThanEqual; + } if (type === EvalFunction.IsWithinRange) { return EvalFunction.IsOutsideRange; } if (type === EvalFunction.IsOutsideRange) { return EvalFunction.IsWithinRange; } + if (type === EvalFunction.IsWithinRangeIncluded) { + return EvalFunction.IsOutsideRangeIncluded; + } + if (type === EvalFunction.IsOutsideRangeIncluded) { + return EvalFunction.IsWithinRangeIncluded; + } return EvalFunction.IsBelow; } @@ -126,7 +144,12 @@ export function isInvalid(condition: ClassicCondition) { const { type, params: loadParams } = evaluator; const { params: unloadParams } = unloadEvaluator; - if (type === EvalFunction.IsWithinRange || type === EvalFunction.IsOutsideRange) { + if ( + type === EvalFunction.IsWithinRange || + type === EvalFunction.IsOutsideRange || + type === EvalFunction.IsWithinRangeIncluded || + type === EvalFunction.IsOutsideRangeIncluded + ) { if (unloadParams[0] === undefined || Number.isNaN(unloadParams[0])) { return { errorMsgFrom: 'This value cannot be empty' }; } @@ -149,6 +172,26 @@ export function isInvalid(condition: ClassicCondition) { return { errorMsg: `Enter a number more than or equal to ${firstParamInEvaluator}` }; } break; + case EvalFunction.IsEqual: + if (firstParamInUnloadEvaluator === firstParamInEvaluator) { + return { errorMsg: `Enter a different number than ${firstParamInEvaluator}` }; + } + break; + case EvalFunction.IsNotEqual: + if (firstParamInUnloadEvaluator !== firstParamInEvaluator) { + return { errorMsg: `Enter the same number as ${firstParamInEvaluator}` }; + } + break; + case EvalFunction.IsGreaterThanEqual: + if (firstParamInUnloadEvaluator >= firstParamInEvaluator) { + return { errorMsg: `Enter a number less than ${firstParamInEvaluator}` }; + } + break; + case EvalFunction.IsLessThanEqual: + if (firstParamInUnloadEvaluator <= firstParamInEvaluator) { + return { errorMsg: `Enter a number more than ${firstParamInEvaluator}` }; + } + break; case EvalFunction.IsOutsideRange: if (firstParamInUnloadEvaluator < firstParamInEvaluator) { return { errorMsgFrom: `Enter a number more than or equal to ${firstParamInEvaluator}` }; @@ -165,6 +208,22 @@ export function isInvalid(condition: ClassicCondition) { return { errorMsgTo: `Enter a number be more than or equal to ${secondParamInEvaluator}` }; } break; + case EvalFunction.IsOutsideRangeIncluded: + if (firstParamInUnloadEvaluator <= firstParamInEvaluator) { + return { errorMsgFrom: `Enter a number more than ${firstParamInEvaluator}` }; + } + if (secondParamInUnloadEvaluator >= secondParamInEvaluator) { + return { errorMsgTo: `Enter a number less than ${secondParamInEvaluator}` }; + } + break; + case EvalFunction.IsWithinRangeIncluded: + if (firstParamInUnloadEvaluator >= firstParamInEvaluator) { + return { errorMsgFrom: `Enter a number less than ${firstParamInEvaluator}` }; + } + if (secondParamInUnloadEvaluator <= secondParamInEvaluator) { + return { errorMsgTo: `Enter a number be more than ${secondParamInEvaluator}` }; + } + break; default: throw new Error(`evaluator function type ${type} not supported.`); } diff --git a/public/app/features/expressions/types.ts b/public/app/features/expressions/types.ts index a1f88729c1e..a132670f00f 100644 --- a/public/app/features/expressions/types.ts +++ b/public/app/features/expressions/types.ts @@ -126,8 +126,14 @@ export const upsamplingTypes: Array> = [ export const thresholdFunctions: Array> = [ { value: EvalFunction.IsAbove, label: 'Is above' }, { value: EvalFunction.IsBelow, label: 'Is below' }, + { value: EvalFunction.IsEqual, label: 'Is equal to' }, + { value: EvalFunction.IsNotEqual, label: 'Is not equal to' }, + { value: EvalFunction.IsGreaterThanEqual, label: 'Is above or equal to' }, + { value: EvalFunction.IsLessThanEqual, label: 'Is below or equal to' }, { value: EvalFunction.IsWithinRange, label: 'Is within range' }, { value: EvalFunction.IsOutsideRange, label: 'Is outside range' }, + { value: EvalFunction.IsWithinRangeIncluded, label: 'Is within range included' }, + { value: EvalFunction.IsOutsideRangeIncluded, label: 'Is outside range included' }, ]; /** diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 88c3d0aa34b..0e93bc677a7 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -528,6 +528,19 @@ }, "pause": { "label": "Pause evaluation" + }, + "threshold": { + "recovery": { + "stop-alerting-above": "Stop alerting when above", + "stop-alerting-bellow": "Stop alerting when below", + "stop-alerting-equal": "Stop alerting when equal to", + "stop-alerting-inside-range": "Stop alerting when inside range", + "stop-alerting-less": "Stop alerting when less than", + "stop-alerting-more": "Stop alerting when more than", + "stop-alerting-not-equal": "Stop alerting when not equal to", + "stop-alerting-outside-range": "Stop alerting when outside range", + "title": "Custom recovery threshold" + } } }, "rule-groups": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index f4b2de7e6dc..583a6e6832f 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -528,6 +528,19 @@ }, "pause": { "label": "Päūşę ęväľūäŧįőʼn" + }, + "threshold": { + "recovery": { + "stop-alerting-above": "Ŝŧőp äľęřŧįʼnģ ŵĥęʼn äþővę", + "stop-alerting-bellow": "Ŝŧőp äľęřŧįʼnģ ŵĥęʼn þęľőŵ", + "stop-alerting-equal": "Ŝŧőp äľęřŧįʼnģ ŵĥęʼn ęqūäľ ŧő", + "stop-alerting-inside-range": "Ŝŧőp äľęřŧįʼnģ ŵĥęʼn įʼnşįđę řäʼnģę", + "stop-alerting-less": "Ŝŧőp äľęřŧįʼnģ ŵĥęʼn ľęşş ŧĥäʼn", + "stop-alerting-more": "Ŝŧőp äľęřŧįʼnģ ŵĥęʼn mőřę ŧĥäʼn", + "stop-alerting-not-equal": "Ŝŧőp äľęřŧįʼnģ ŵĥęʼn ʼnőŧ ęqūäľ ŧő", + "stop-alerting-outside-range": "Ŝŧőp äľęřŧįʼnģ ŵĥęʼn őūŧşįđę řäʼnģę", + "title": "Cūşŧőm řęčővęřy ŧĥřęşĥőľđ" + } } }, "rule-groups": {