Skip to content

Commit e61d9ca

Browse files
committed
Add severity level to Inventory alert
1 parent 111b3d9 commit e61d9ca

File tree

13 files changed

+369
-140
lines changed

13 files changed

+369
-140
lines changed

x-pack/plugins/infra/common/alerting/metrics/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ export const INFRA_ALERT_PREVIEW_PATH = '/api/infra/alerting/preview';
99

1010
export const TOO_MANY_BUCKETS_PREVIEW_EXCEPTION = 'TOO_MANY_BUCKETS_PREVIEW_EXCEPTION';
1111
export interface TooManyBucketsPreviewExceptionMetadata {
12-
TOO_MANY_BUCKETS_PREVIEW_EXCEPTION: any;
13-
maxBuckets: number;
12+
TOO_MANY_BUCKETS_PREVIEW_EXCEPTION: boolean;
13+
maxBuckets: any;
1414
}
1515
export const isTooManyBucketsPreviewException = (
1616
value: any

x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx

Lines changed: 164 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { debounce, pick } from 'lodash';
7+
import { debounce, pick, omit } from 'lodash';
88
import { Unit } from '@elastic/datemath';
99
import React, { useCallback, useMemo, useEffect, useState, ChangeEvent } from 'react';
1010
import { IFieldType } from 'src/plugins/data/public';
@@ -20,6 +20,7 @@ import {
2020
EuiCheckbox,
2121
EuiToolTip,
2222
EuiIcon,
23+
EuiHealth,
2324
} from '@elastic/eui';
2425
import { FormattedMessage } from '@kbn/i18n/react';
2526
import { i18n } from '@kbn/i18n';
@@ -422,9 +423,24 @@ const StyledExpression = euiStyled.div`
422423
padding: 0 4px;
423424
`;
424425

426+
const StyledHealth = euiStyled(EuiHealth)`
427+
margin-left: 4px;
428+
`;
429+
425430
export const ExpressionRow: React.FC<ExpressionRowProps> = (props) => {
426431
const { setAlertParams, expression, errors, expressionId, remove, canDelete, fields } = props;
427-
const { metric, comparator = Comparator.GT, threshold = [], customMetric } = expression;
432+
const {
433+
metric,
434+
comparator = Comparator.GT,
435+
threshold = [],
436+
customMetric,
437+
warningThreshold = [],
438+
warningComparator,
439+
} = expression;
440+
441+
const [displayWarningThreshold, setDisplayWarningThreshold] = useState(
442+
Boolean(warningThreshold?.length)
443+
);
428444

429445
const updateMetric = useCallback(
430446
(m?: SnapshotMetricType | string) => {
@@ -451,6 +467,13 @@ export const ExpressionRow: React.FC<ExpressionRowProps> = (props) => {
451467
[expressionId, expression, setAlertParams]
452468
);
453469

470+
const updateWarningComparator = useCallback(
471+
(c?: string) => {
472+
setAlertParams(expressionId, { ...expression, warningComparator: c as Comparator });
473+
},
474+
[expressionId, expression, setAlertParams]
475+
);
476+
454477
const updateThreshold = useCallback(
455478
(t) => {
456479
if (t.join() !== expression.threshold.join()) {
@@ -460,6 +483,58 @@ export const ExpressionRow: React.FC<ExpressionRowProps> = (props) => {
460483
[expressionId, expression, setAlertParams]
461484
);
462485

486+
const updateWarningThreshold = useCallback(
487+
(t) => {
488+
if (t.join() !== expression.warningThreshold?.join()) {
489+
setAlertParams(expressionId, { ...expression, warningThreshold: t });
490+
}
491+
},
492+
[expressionId, expression, setAlertParams]
493+
);
494+
495+
const toggleWarningThreshold = useCallback(() => {
496+
if (!displayWarningThreshold) {
497+
setDisplayWarningThreshold(true);
498+
setAlertParams(expressionId, {
499+
...expression,
500+
warningComparator: comparator,
501+
warningThreshold: [],
502+
});
503+
} else {
504+
setDisplayWarningThreshold(false);
505+
setAlertParams(expressionId, omit(expression, 'warningComparator', 'warningThreshold'));
506+
}
507+
}, [
508+
displayWarningThreshold,
509+
setDisplayWarningThreshold,
510+
setAlertParams,
511+
comparator,
512+
expression,
513+
expressionId,
514+
]);
515+
516+
const criticalThresholdExpression = (
517+
<ThresholdElement
518+
comparator={comparator}
519+
threshold={threshold}
520+
updateComparator={updateComparator}
521+
updateThreshold={updateThreshold}
522+
errors={errors.critical as IErrorObject}
523+
metric={metric}
524+
/>
525+
);
526+
527+
const warningThresholdExpression = displayWarningThreshold && (
528+
<ThresholdElement
529+
comparator={warningComparator || comparator}
530+
threshold={warningThreshold}
531+
updateComparator={updateWarningComparator}
532+
updateThreshold={updateWarningThreshold}
533+
errors={errors.warning as IErrorObject}
534+
metric={metric}
535+
/>
536+
);
537+
463538
const ofFields = useMemo(() => {
464539
let myMetrics = hostMetricTypes;
465540

@@ -514,25 +589,62 @@ export const ExpressionRow: React.FC<ExpressionRowProps> = (props) => {
514589
fields={fields}
515590
/>
516591
</StyledExpression>
517-
<StyledExpression>
518-
<ThresholdExpression
519-
thresholdComparator={comparator || Comparator.GT}
520-
threshold={threshold}
521-
onChangeSelectedThresholdComparator={updateComparator}
522-
onChangeSelectedThreshold={updateThreshold}
523-
errors={errors}
524-
/>
525-
</StyledExpression>
526-
{metric && (
527-
<div
528-
style={{
529-
alignSelf: 'center',
530-
}}
531-
>
532-
<EuiText size={'s'}>{metricUnit[metric]?.label || ''}</EuiText>
533-
</div>
534-
)}
592+
{!displayWarningThreshold && criticalThresholdExpression}
535593
</StyledExpressionRow>
594+
{displayWarningThreshold && (
595+
<>
596+
<StyledExpressionRow>
597+
{criticalThresholdExpression}
598+
<StyledHealth color="danger">
599+
<FormattedMessage
600+
id="xpack.infra.metrics.alertFlyout.criticalThreshold"
601+
defaultMessage="Alert"
602+
/>
603+
</StyledHealth>
604+
</StyledExpressionRow>
605+
<StyledExpressionRow>
606+
{warningThresholdExpression}
607+
<StyledHealth color="warning">
608+
<FormattedMessage
609+
id="xpack.infra.metrics.alertFlyout.warningThreshold"
610+
defaultMessage="Warning"
611+
/>
612+
</StyledHealth>
613+
<EuiButtonIcon
614+
aria-label={i18n.translate(
615+
'xpack.infra.metrics.alertFlyout.removeWarningThreshold',
616+
{
617+
defaultMessage: 'Remove warningThreshold',
618+
}
619+
)}
620+
iconSize="s"
621+
color={'subdued'}
622+
iconType={'crossInACircleFilled'}
623+
onClick={toggleWarningThreshold}
624+
/>
625+
</StyledExpressionRow>
626+
</>
627+
)}
628+
{!displayWarningThreshold && (
629+
<>
630+
{' '}
631+
<EuiSpacer size={'xs'} />
632+
<StyledExpressionRow>
633+
<EuiButtonEmpty
634+
color={'primary'}
635+
flush={'left'}
636+
size="xs"
637+
iconType={'plusInCircleFilled'}
638+
onClick={toggleWarningThreshold}
639+
>
640+
<FormattedMessage
641+
id="xpack.infra.metrics.alertFlyout.addWarningThreshold"
642+
defaultMessage="Add warning threshold"
643+
/>
644+
</EuiButtonEmpty>
645+
</StyledExpressionRow>
646+
</>
647+
)}
536648
</EuiFlexItem>
537649
{canDelete && (
538650
<EuiFlexItem grow={false}>
@@ -552,6 +664,38 @@ export const ExpressionRow: React.FC<ExpressionRowProps> = (props) => {
552664
);
553665
};
554666

667+
const ThresholdElement: React.FC<{
668+
updateComparator: (c?: string) => void;
669+
updateThreshold: (t?: number[]) => void;
670+
threshold: InventoryMetricConditions['threshold'];
671+
comparator: InventoryMetricConditions['comparator'];
672+
errors: IErrorObject;
673+
metric?: SnapshotMetricType;
674+
}> = ({ updateComparator, updateThreshold, threshold, metric, comparator, errors }) => {
675+
return (
676+
<>
677+
<StyledExpression>
678+
<ThresholdExpression
679+
thresholdComparator={comparator || Comparator.GT}
680+
threshold={threshold}
681+
onChangeSelectedThresholdComparator={updateComparator}
682+
onChangeSelectedThreshold={updateThreshold}
683+
errors={errors}
684+
/>
685+
</StyledExpression>
686+
{metric && (
687+
<div
688+
style={{
689+
alignSelf: 'center',
690+
}}
691+
>
692+
<EuiText size={'s'}>{metricUnit[metric]?.label || ''}</EuiText>
693+
</div>
694+
)}
695+
</>
696+
);
697+
};
698+
555699
const getDisplayNameForType = (type: InventoryItemType) => {
556700
const inventoryModel = findInventoryModel(type);
557701
return inventoryModel.displayName;

x-pack/plugins/infra/public/alerting/inventory/components/validation.tsx

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
*/
66

77
import { i18n } from '@kbn/i18n';
8-
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
9-
import { InventoryMetricConditions } from '../../../../server/lib/alerting/inventory_metric_threshold/types';
8+
import {
9+
InventoryMetricConditions,
10+
Comparator,
11+
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
12+
} from '../../../../server/lib/alerting/inventory_metric_threshold/types';
1013
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
1114
import { ValidationResult } from '../../../../../triggers_actions_ui/public/types';
1215

@@ -20,8 +23,14 @@ export function validateMetricThreshold({
2023
[id: string]: {
2124
timeSizeUnit: string[];
2225
timeWindowSize: string[];
23-
threshold0: string[];
24-
threshold1: string[];
26+
critical: {
27+
threshold0: string[];
28+
threshold1: string[];
29+
};
30+
warning: {
31+
threshold0: string[];
32+
threshold1: string[];
33+
};
2534
metric: string[];
2635
};
2736
} = {};
@@ -38,41 +47,64 @@ export function validateMetricThreshold({
3847
errors[id] = errors[id] || {
3948
timeSizeUnit: [],
4049
timeWindowSize: [],
41-
threshold0: [],
42-
threshold1: [],
50+
critical: {
51+
threshold0: [],
52+
threshold1: [],
53+
},
54+
warning: {
55+
threshold0: [],
56+
threshold1: [],
57+
},
4358
metric: [],
4459
};
4560

4661
if (!c.threshold || !c.threshold.length) {
47-
errors[id].threshold0.push(
62+
errors[id].critical.threshold0.push(
4863
i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', {
4964
defaultMessage: 'Threshold is required.',
5065
})
5166
);
5267
}
5368

54-
// The Threshold component returns an empty array with a length ([empty]) because it's using delete newThreshold[i].
55-
// We need to use [...c.threshold] to convert it to an array with an undefined value ([undefined]) so we can test each element.
56-
if (c.threshold && c.threshold.length && ![...c.threshold].every(isNumber)) {
57-
[...c.threshold].forEach((v, i) => {
58-
if (!isNumber(v)) {
59-
const key = i === 0 ? 'threshold0' : 'threshold1';
60-
errors[id][key].push(
61-
i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdTypeRequired', {
62-
defaultMessage: 'Thresholds must contain a valid number.',
63-
})
64-
);
65-
}
66-
});
67-
}
68-
69-
if (c.comparator === 'between' && (!c.threshold || c.threshold.length < 2)) {
70-
errors[id].threshold1.push(
69+
if (c.warningThreshold && !c.warningThreshold.length) {
70+
errors[id].warning.threshold0.push(
7171
i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', {
7272
defaultMessage: 'Threshold is required.',
7373
})
7474
);
7575
}
76+
for (const props of [
77+
{ comparator: c.comparator, threshold: c.threshold, type: 'critical' },
78+
{ comparator: c.warningComparator, threshold: c.warningThreshold, type: 'warning' },
79+
]) {
80+
// The Threshold component returns an empty array with a length ([empty]) because it's using delete newThreshold[i].
81+
// We need to use [...c.threshold] to convert it to an array with an undefined value ([undefined]) so we can test each element.
82+
const { comparator, threshold, type } = props as {
83+
comparator?: Comparator;
84+
threshold?: number[];
85+
type: 'critical' | 'warning';
86+
};
87+
if (threshold && threshold.length && ![...threshold].every(isNumber)) {
88+
[...threshold].forEach((v, i) => {
89+
if (!isNumber(v)) {
90+
const key = i === 0 ? 'threshold0' : 'threshold1';
91+
errors[id][type][key].push(
92+
i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdTypeRequired', {
93+
defaultMessage: 'Thresholds must contain a valid number.',
94+
})
95+
);
96+
}
97+
});
98+
}
99+
100+
if (comparator === Comparator.BETWEEN && (!threshold || threshold.length < 2)) {
101+
errors[id][type].threshold1.push(
102+
i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', {
103+
defaultMessage: 'Threshold is required.',
104+
})
105+
);
106+
}
107+
}
76108

77109
if (!c.timeSize) {
78110
errors[id].timeWindowSize.push(

x-pack/plugins/infra/server/lib/alerting/common/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,11 @@ export enum AlertStates {
3232
NO_DATA,
3333
ERROR,
3434
}
35+
36+
export interface PreviewResult {
37+
fired: number;
38+
warning: number;
39+
noData: number;
40+
error: number;
41+
notifications: number;
42+
}

0 commit comments

Comments
 (0)