Alerting: Add multiple threshold operators (#99516)

The following operators are being added:
- Equal
- Not Equal
- Greater or Equal
- Less or Equal
- Within Range Inclusive
- Outside Range Inclusive
This commit is contained in:
Paulo Dias
2025-02-21 19:11:16 +02:00
committed by GitHub
parent 6ebde0481e
commit 2d2e595555
18 changed files with 781 additions and 128 deletions
+1 -6
View File
@@ -4991,12 +4991,7 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"]
],
"public/app/features/expressions/components/Threshold.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "5"]
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> 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"],
+14 -2
View File
@@ -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
+42
View File
@@ -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) {
+15 -3
View File
@@ -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#"
}
}
+15 -3
View File
@@ -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#"
}
}
+15 -3
View File
@@ -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 @@
}
}
]
}
}
+96 -4
View File
@@ -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
}
+90
View File
@@ -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,
@@ -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;
}
@@ -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' },
];
@@ -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
);
}
@@ -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
);
}
@@ -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 (
<Stack>
@@ -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
<div onMouseDown={onHysteresisCheckDown}>
<InlineSwitch
showLabel={true}
label="Custom recovery threshold"
label={t('alerting.rule-form.threshold.recovery.title', 'Custom recovery threshold')}
value={hasHysteresis}
onChange={onHysteresisCheckChange}
className={styles.switch}
@@ -211,111 +214,286 @@ function RecoveryThresholdRow({ isRange, condition, onError, dispatch, allowOnbl
}
function RecoveryForRange({ allowOnblur }: RecoveryProps) {
if (condition.evaluator.type === EvalFunction.IsWithinRange) {
return (
<InlineFieldRow className={styles.hysteresis}>
<InlineField label="Stop alerting when outside range" labelWidth={'auto'}>
<Stack direction="row" gap={0}>
<div className={styles.range}>
<InlineField invalid={Boolean(errorMsgFrom)} error={errorMsgFrom} className={styles.noMargin}>
<Input
type="number"
width={10}
onBlur={(event) => allowOnblur.current && onUnloadValueChange(event, 0)}
defaultValue={condition.unloadEvaluator?.params[0]}
/>
</InlineField>
</div>
<ToLabel />
<div className={styles.range}>
<InlineField invalid={Boolean(errorMsgTo)} error={errorMsgTo}>
<Input
type="number"
width={10}
onBlur={(event) => allowOnblur.current && onUnloadValueChange(event, 1)}
defaultValue={condition.unloadEvaluator?.params[1]}
/>
</InlineField>
</div>
</Stack>
</InlineField>
</InlineFieldRow>
);
} else {
return (
<InlineFieldRow className={styles.hysteresis}>
<InlineField label="Stop alerting when inside range" labelWidth={'auto'}>
<Stack direction="row" gap={0}>
<div className={styles.range}>
<InlineField invalid={Boolean(errorMsgFrom)} error={errorMsgFrom}>
<Input
type="number"
width={10}
onBlur={(event) => allowOnblur.current && onUnloadValueChange(event, 0)}
defaultValue={condition.unloadEvaluator?.params[0]}
/>
</InlineField>
</div>
switch (condition.evaluator.type) {
case EvalFunction.IsWithinRange:
if (condition.evaluator.type === EvalFunction.IsWithinRange) {
return (
<InlineFieldRow className={styles.hysteresis}>
<InlineField
label={t(
'alerting.rule-form.threshold.recovery.stop-alerting-outside-range',
'Stop alerting when outside range'
)}
labelWidth={'auto'}
>
<Stack direction="row" gap={0}>
<div className={styles.range}>
<InlineField invalid={Boolean(errorMsgFrom)} error={errorMsgFrom} className={styles.noMargin}>
<Input
type="number"
width={10}
onBlur={(event) => allowOnblur.current && onUnloadValueChange(event, 0)}
defaultValue={condition.unloadEvaluator?.params[0]}
/>
</InlineField>
</div>
<ToLabel />
<div className={styles.range}>
<InlineField invalid={Boolean(errorMsgTo)} error={errorMsgTo}>
<Input
type="number"
width={10}
onBlur={(event) => allowOnblur.current && onUnloadValueChange(event, 1)}
defaultValue={condition.unloadEvaluator?.params[1]}
/>
</InlineField>
</div>
</Stack>
</InlineField>
</InlineFieldRow>
);
}
case EvalFunction.IsOutsideRange:
return (
<InlineFieldRow className={styles.hysteresis}>
<InlineField
label={t(
'alerting.rule-form.threshold.recovery.stop-alerting-inside-range',
'Stop alerting when inside range'
)}
labelWidth={'auto'}
>
<Stack direction="row" gap={0}>
<div className={styles.range}>
<InlineField invalid={Boolean(errorMsgFrom)} error={errorMsgFrom}>
<Input
type="number"
width={10}
onBlur={(event) => allowOnblur.current && onUnloadValueChange(event, 0)}
defaultValue={condition.unloadEvaluator?.params[0]}
/>
</InlineField>
</div>
<ToLabel />
<div className={styles.range}>
<InlineField invalid={Boolean(errorMsgTo)} error={errorMsgTo}>
<Input
type="number"
width={10}
onBlur={(event) => allowOnblur.current && onUnloadValueChange(event, 1)}
defaultValue={condition.unloadEvaluator?.params[1]}
/>
</InlineField>
</div>
</Stack>
</InlineField>
</InlineFieldRow>
);
<ToLabel />
<div className={styles.range}>
<InlineField invalid={Boolean(errorMsgTo)} error={errorMsgTo}>
<Input
type="number"
width={10}
onBlur={(event) => allowOnblur.current && onUnloadValueChange(event, 1)}
defaultValue={condition.unloadEvaluator?.params[1]}
/>
</InlineField>
</div>
</Stack>
</InlineField>
</InlineFieldRow>
);
case EvalFunction.IsOutsideRangeIncluded:
return (
<InlineFieldRow className={styles.hysteresis}>
<InlineField
label={t(
'alerting.rule-form.threshold.recovery.stop-alerting-inside-range',
'Stop alerting when inside range'
)}
labelWidth={'auto'}
>
<Stack direction="row" gap={0}>
<div className={styles.range}>
<InlineField invalid={Boolean(errorMsgFrom)} error={errorMsgFrom}>
<Input
type="number"
width={10}
onBlur={(event) => allowOnblur.current && onUnloadValueChange(event, 0)}
defaultValue={condition.unloadEvaluator?.params[0]}
/>
</InlineField>
</div>
<ToLabel />
<div className={styles.range}>
<InlineField invalid={Boolean(errorMsgTo)} error={errorMsgTo}>
<Input
type="number"
width={10}
onBlur={(event) => allowOnblur.current && onUnloadValueChange(event, 1)}
defaultValue={condition.unloadEvaluator?.params[1]}
/>
</InlineField>
</div>
</Stack>
</InlineField>
</InlineFieldRow>
);
case EvalFunction.IsWithinRangeIncluded:
return (
<InlineFieldRow className={styles.hysteresis}>
<InlineField
label={t(
'alerting.rule-form.threshold.recovery.stop-alerting-outside-range',
'Stop alerting when outside range'
)}
labelWidth={'auto'}
>
<Stack direction="row" gap={0}>
<div className={styles.range}>
<InlineField invalid={Boolean(errorMsgFrom)} error={errorMsgFrom}>
<Input
type="number"
width={10}
onBlur={(event) => allowOnblur.current && onUnloadValueChange(event, 0)}
defaultValue={condition.unloadEvaluator?.params[0]}
/>
</InlineField>
</div>
<ToLabel />
<div className={styles.range}>
<InlineField invalid={Boolean(errorMsgTo)} error={errorMsgTo}>
<Input
type="number"
width={10}
onBlur={(event) => allowOnblur.current && onUnloadValueChange(event, 1)}
defaultValue={condition.unloadEvaluator?.params[1]}
/>
</InlineField>
</div>
</Stack>
</InlineField>
</InlineFieldRow>
);
default:
return null;
}
}
function RecoveryForSingleValue({ allowOnblur }: RecoveryProps) {
if (condition.evaluator.type === EvalFunction.IsAbove) {
return (
<InlineFieldRow className={styles.hysteresis}>
<InlineField
label="Stop alerting when below"
labelWidth={'auto'}
invalid={Boolean(invalidErrorMsg)}
error={invalidErrorMsg}
>
<Input
type="number"
width={10}
onBlur={(event) => {
allowOnblur.current && onUnloadValueChange(event, 0);
}}
defaultValue={condition.unloadEvaluator?.params[0]}
/>
</InlineField>
</InlineFieldRow>
);
} else {
return (
<InlineFieldRow className={styles.hysteresis}>
<InlineField
label="Stop alerting when above"
labelWidth={'auto'}
invalid={Boolean(invalidErrorMsg)}
error={invalidErrorMsg}
>
<Input
type="number"
width={10}
onBlur={(event) => {
allowOnblur.current && onUnloadValueChange(event, 0);
}}
defaultValue={condition.unloadEvaluator?.params[0]}
/>
</InlineField>
</InlineFieldRow>
);
switch (condition.evaluator.type) {
case EvalFunction.IsAbove:
return (
<InlineFieldRow className={styles.hysteresis}>
<InlineField
label={t('alerting.rule-form.threshold.recovery.stop-alerting-bellow', 'Stop alerting when below')}
labelWidth={'auto'}
invalid={Boolean(invalidErrorMsg)}
error={invalidErrorMsg}
>
<Input
type="number"
width={10}
onBlur={(event) => {
allowOnblur.current && onUnloadValueChange(event, 0);
}}
defaultValue={condition.unloadEvaluator?.params[0]}
/>
</InlineField>
</InlineFieldRow>
);
case EvalFunction.IsBelow:
return (
<InlineFieldRow className={styles.hysteresis}>
<InlineField
label={t('alerting.rule-form.threshold.recovery.stop-alerting-above', 'Stop alerting when above')}
labelWidth={'auto'}
invalid={Boolean(invalidErrorMsg)}
error={invalidErrorMsg}
>
<Input
type="number"
width={10}
onBlur={(event) => {
allowOnblur.current && onUnloadValueChange(event, 0);
}}
defaultValue={condition.unloadEvaluator?.params[0]}
/>
</InlineField>
</InlineFieldRow>
);
case EvalFunction.IsEqual:
return (
<InlineFieldRow className={styles.hysteresis}>
<InlineField
label={t('alerting.rule-form.threshold.recovery.stop-alerting-equal', 'Stop alerting when equal to')}
labelWidth={'auto'}
invalid={Boolean(invalidErrorMsg)}
error={invalidErrorMsg}
>
<Input
type="number"
width={10}
onBlur={(event) => {
allowOnblur.current && onUnloadValueChange(event, 0);
}}
defaultValue={condition.unloadEvaluator?.params[0]}
/>
</InlineField>
</InlineFieldRow>
);
case EvalFunction.IsNotEqual:
return (
<InlineFieldRow className={styles.hysteresis}>
<InlineField
label={t(
'alerting.rule-form.threshold.recovery.stop-alerting-not-equal',
'Stop alerting when not equal to'
)}
labelWidth={'auto'}
invalid={Boolean(invalidErrorMsg)}
error={invalidErrorMsg}
>
<Input
type="number"
width={10}
onBlur={(event) => {
allowOnblur.current && onUnloadValueChange(event, 0);
}}
defaultValue={condition.unloadEvaluator?.params[0]}
/>
</InlineField>
</InlineFieldRow>
);
case EvalFunction.IsGreaterThanEqual:
return (
<InlineFieldRow className={styles.hysteresis}>
<InlineField
label={t('alerting.rule-form.threshold.recovery.stop-alerting-less', 'Stop alerting when less than')}
labelWidth={'auto'}
invalid={Boolean(invalidErrorMsg)}
error={invalidErrorMsg}
>
<Input
type="number"
width={10}
onBlur={(event) => {
allowOnblur.current && onUnloadValueChange(event, 0);
}}
defaultValue={condition.unloadEvaluator?.params[0]}
/>
</InlineField>
</InlineFieldRow>
);
case EvalFunction.IsLessThanEqual:
return (
<InlineFieldRow className={styles.hysteresis}>
<InlineField
label={t('alerting.rule-form.threshold.recovery.stop-alerting-more', 'Stop alerting when more than')}
labelWidth={'auto'}
invalid={Boolean(invalidErrorMsg)}
error={invalidErrorMsg}
>
<Input
type="number"
width={10}
onBlur={(event) => {
allowOnblur.current && onUnloadValueChange(event, 0);
}}
defaultValue={condition.unloadEvaluator?.params[0]}
/>
</InlineField>
</InlineFieldRow>
);
default:
return null;
}
}
}
@@ -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.`);
}
+6
View File
@@ -126,8 +126,14 @@ export const upsamplingTypes: Array<SelectableValue<string>> = [
export const thresholdFunctions: Array<SelectableValue<EvalFunction>> = [
{ 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' },
];
/**
+13
View File
@@ -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": {
+13
View File
@@ -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": {