Skip to content

Commit 8aa7e13

Browse files
authored
[Alerting] Adds generic UI for the definition of conditions for Action Groups (#83278)
This PR adds two components to aid in creating a uniform UI for specifying the conditions for Action Groups: 1. `AlertConditions`: A component that generates a container which renders custom component for each Action Group which has had its _conditions_ specified. 2. `AlertConditionsGroup`: A component that provides a unified container for the Action Group with its name and a button for resetting its condition. This can be used by any Alert Type to easily create the UI for adding action groups with whichever UI is specific to their component.
1 parent 63cb5ae commit 8aa7e13

File tree

22 files changed

+1062
-116
lines changed

22 files changed

+1062
-116
lines changed

x-pack/examples/alerting_example/common/constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ export const ALERTING_EXAMPLE_APP_ID = 'AlertingExample';
88

99
// always firing
1010
export const DEFAULT_INSTANCES_TO_GENERATE = 5;
11+
export interface AlwaysFiringParams {
12+
instances?: number;
13+
thresholds?: {
14+
small?: number;
15+
medium?: number;
16+
large?: number;
17+
};
18+
}
19+
export type AlwaysFiringActionGroupIds = keyof AlwaysFiringParams['thresholds'];
1120

1221
// Astros
1322
export enum Craft {

x-pack/examples/alerting_example/public/alert_types/always_firing.tsx

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

7-
import React, { Fragment } from 'react';
8-
import { EuiFlexGroup, EuiFlexItem, EuiFieldNumber, EuiFormRow } from '@elastic/eui';
7+
import React, { Fragment, useState } from 'react';
8+
import {
9+
EuiFlexGroup,
10+
EuiFlexItem,
11+
EuiFieldNumber,
12+
EuiFormRow,
13+
EuiPopover,
14+
EuiExpression,
15+
EuiSpacer,
16+
} from '@elastic/eui';
917
import { i18n } from '@kbn/i18n';
10-
import { AlertTypeModel } from '../../../../plugins/triggers_actions_ui/public';
11-
import { DEFAULT_INSTANCES_TO_GENERATE } from '../../common/constants';
12-
13-
interface AlwaysFiringParamsProps {
14-
alertParams: { instances?: number };
15-
setAlertParams: (property: string, value: any) => void;
16-
errors: { [key: string]: string[] };
17-
}
18+
import { omit, pick } from 'lodash';
19+
import {
20+
ActionGroupWithCondition,
21+
AlertConditions,
22+
AlertConditionsGroup,
23+
AlertTypeModel,
24+
AlertTypeParamsExpressionProps,
25+
AlertsContextValue,
26+
} from '../../../../plugins/triggers_actions_ui/public';
27+
import {
28+
AlwaysFiringParams,
29+
AlwaysFiringActionGroupIds,
30+
DEFAULT_INSTANCES_TO_GENERATE,
31+
} from '../../common/constants';
1832

1933
export function getAlertType(): AlertTypeModel {
2034
return {
@@ -24,7 +38,7 @@ export function getAlertType(): AlertTypeModel {
2438
iconClass: 'bolt',
2539
documentationUrl: null,
2640
alertParamsExpression: AlwaysFiringExpression,
27-
validate: (alertParams: AlwaysFiringParamsProps['alertParams']) => {
41+
validate: (alertParams: AlwaysFiringParams) => {
2842
const { instances } = alertParams;
2943
const validationResult = {
3044
errors: {
@@ -44,11 +58,30 @@ export function getAlertType(): AlertTypeModel {
4458
};
4559
}
4660

47-
export const AlwaysFiringExpression: React.FunctionComponent<AlwaysFiringParamsProps> = ({
48-
alertParams,
49-
setAlertParams,
50-
}) => {
51-
const { instances = DEFAULT_INSTANCES_TO_GENERATE } = alertParams;
61+
const DEFAULT_THRESHOLDS: AlwaysFiringParams['thresholds'] = {
62+
small: 0,
63+
medium: 5000,
64+
large: 10000,
65+
};
66+
67+
export const AlwaysFiringExpression: React.FunctionComponent<AlertTypeParamsExpressionProps<
68+
AlwaysFiringParams,
69+
AlertsContextValue
70+
>> = ({ alertParams, setAlertParams, actionGroups, defaultActionGroupId }) => {
71+
const {
72+
instances = DEFAULT_INSTANCES_TO_GENERATE,
73+
thresholds = pick(DEFAULT_THRESHOLDS, defaultActionGroupId),
74+
} = alertParams;
75+
76+
const actionGroupsWithConditions = actionGroups.map((actionGroup) =>
77+
Number.isInteger(thresholds[actionGroup.id as AlwaysFiringActionGroupIds])
78+
? {
79+
...actionGroup,
80+
conditions: thresholds[actionGroup.id as AlwaysFiringActionGroupIds]!,
81+
}
82+
: actionGroup
83+
);
84+
5285
return (
5386
<Fragment>
5487
<EuiFlexGroup gutterSize="s" wrap direction="column">
@@ -67,6 +100,88 @@ export const AlwaysFiringExpression: React.FunctionComponent<AlwaysFiringParamsP
67100
</EuiFormRow>
68101
</EuiFlexItem>
69102
</EuiFlexGroup>
103+
<EuiSpacer size="m" />
104+
<EuiFlexGroup>
105+
<EuiFlexItem grow={true}>
106+
<AlertConditions
107+
headline={'Set different thresholds for randomly generated T-Shirt sizes'}
108+
actionGroups={actionGroupsWithConditions}
109+
onInitializeConditionsFor={(actionGroup) => {
110+
setAlertParams('thresholds', {
111+
...thresholds,
112+
...pick(DEFAULT_THRESHOLDS, actionGroup.id),
113+
});
114+
}}
115+
>
116+
<AlertConditionsGroup
117+
onResetConditionsFor={(actionGroup) => {
118+
setAlertParams('thresholds', omit(thresholds, actionGroup.id));
119+
}}
120+
>
121+
<TShirtSelector
122+
setTShirtThreshold={(actionGroup) => {
123+
setAlertParams('thresholds', {
124+
...thresholds,
125+
[actionGroup.id]: actionGroup.conditions,
126+
});
127+
}}
128+
/>
129+
</AlertConditionsGroup>
130+
</AlertConditions>
131+
</EuiFlexItem>
132+
</EuiFlexGroup>
133+
<EuiSpacer />
70134
</Fragment>
71135
);
72136
};
137+
138+
interface TShirtSelectorProps {
139+
actionGroup?: ActionGroupWithCondition<number>;
140+
setTShirtThreshold: (actionGroup: ActionGroupWithCondition<number>) => void;
141+
}
142+
const TShirtSelector = ({ actionGroup, setTShirtThreshold }: TShirtSelectorProps) => {
143+
const [isOpen, setIsOpen] = useState(false);
144+
145+
if (!actionGroup) {
146+
return null;
147+
}
148+
149+
return (
150+
<EuiPopover
151+
panelPaddingSize="s"
152+
button={
153+
<EuiExpression
154+
description={'Is Above'}
155+
value={actionGroup.conditions}
156+
isActive={isOpen}
157+
onClick={() => setIsOpen(true)}
158+
/>
159+
}
160+
isOpen={isOpen}
161+
closePopover={() => setIsOpen(false)}
162+
ownFocus
163+
anchorPosition="downLeft"
164+
>
165+
<EuiFlexGroup>
166+
<EuiFlexItem grow={false} style={{ width: 150 }}>
167+
{'Is Above'}
168+
</EuiFlexItem>
169+
<EuiFlexItem grow={false} style={{ width: 100 }}>
170+
<EuiFieldNumber
171+
compressed
172+
value={actionGroup.conditions}
173+
onChange={(e) => {
174+
const conditions = parseInt(e.target.value, 10);
175+
if (e.target.value && !isNaN(conditions)) {
176+
setTShirtThreshold({
177+
...actionGroup,
178+
conditions,
179+
});
180+
}
181+
}}
182+
/>
183+
</EuiFlexItem>
184+
</EuiFlexGroup>
185+
</EuiPopover>
186+
);
187+
};

x-pack/examples/alerting_example/server/alert_types/always_firing.ts

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,56 @@
55
*/
66

77
import uuid from 'uuid';
8-
import { range, random } from 'lodash';
8+
import { range } from 'lodash';
99
import { AlertType } from '../../../../plugins/alerts/server';
10-
import { DEFAULT_INSTANCES_TO_GENERATE, ALERTING_EXAMPLE_APP_ID } from '../../common/constants';
10+
import {
11+
DEFAULT_INSTANCES_TO_GENERATE,
12+
ALERTING_EXAMPLE_APP_ID,
13+
AlwaysFiringParams,
14+
} from '../../common/constants';
1115

1216
const ACTION_GROUPS = [
13-
{ id: 'small', name: 'small' },
14-
{ id: 'medium', name: 'medium' },
15-
{ id: 'large', name: 'large' },
17+
{ id: 'small', name: 'Small t-shirt' },
18+
{ id: 'medium', name: 'Medium t-shirt' },
19+
{ id: 'large', name: 'Large t-shirt' },
1620
];
21+
const DEFAULT_ACTION_GROUP = 'small';
1722

18-
export const alertType: AlertType = {
23+
function getTShirtSizeByIdAndThreshold(id: string, thresholds: AlwaysFiringParams['thresholds']) {
24+
const idAsNumber = parseInt(id, 10);
25+
if (!isNaN(idAsNumber)) {
26+
if (thresholds?.large && thresholds.large < idAsNumber) {
27+
return 'large';
28+
}
29+
if (thresholds?.medium && thresholds.medium < idAsNumber) {
30+
return 'medium';
31+
}
32+
if (thresholds?.small && thresholds.small < idAsNumber) {
33+
return 'small';
34+
}
35+
}
36+
return DEFAULT_ACTION_GROUP;
37+
}
38+
39+
export const alertType: AlertType<AlwaysFiringParams> = {
1940
id: 'example.always-firing',
2041
name: 'Always firing',
2142
actionGroups: ACTION_GROUPS,
22-
defaultActionGroupId: 'small',
23-
async executor({ services, params: { instances = DEFAULT_INSTANCES_TO_GENERATE }, state }) {
43+
defaultActionGroupId: DEFAULT_ACTION_GROUP,
44+
async executor({
45+
services,
46+
params: { instances = DEFAULT_INSTANCES_TO_GENERATE, thresholds },
47+
state,
48+
}) {
2449
const count = (state.count ?? 0) + 1;
2550

2651
range(instances)
27-
.map(() => ({ id: uuid.v4(), tshirtSize: ACTION_GROUPS[random(0, 2)].id! }))
28-
.forEach((instance: { id: string; tshirtSize: string }) => {
52+
.map(() => uuid.v4())
53+
.forEach((id: string) => {
2954
services
30-
.alertInstanceFactory(instance.id)
55+
.alertInstanceFactory(id)
3156
.replaceState({ triggerdOnCycle: count })
32-
.scheduleActions(instance.tshirtSize);
57+
.scheduleActions(getTShirtSizeByIdAndThreshold(id, thresholds));
3358
});
3459

3560
return {

x-pack/plugins/alerts/common/alert.ts

Lines changed: 2 additions & 1 deletion
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 { SavedObjectAttributes } from 'kibana/server';
7+
import { SavedObjectAttribute, SavedObjectAttributes } from 'kibana/server';
88

99
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1010
export type AlertTypeState = Record<string, any>;
@@ -37,6 +37,7 @@ export interface AlertExecutionStatus {
3737
}
3838

3939
export type AlertActionParams = SavedObjectAttributes;
40+
export type AlertActionParam = SavedObjectAttribute;
4041

4142
export interface AlertAction {
4243
group: string;

x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import React, { Fragment, FunctionComponent, useEffect, useRef } from 'react';
88
import { EuiFormRow } from '@elastic/eui';
99
import { FormattedMessage } from '@kbn/i18n/react';
1010
import { i18n } from '@kbn/i18n';
11-
import { IErrorObject, AlertsContextValue } from '../../../../../../triggers_actions_ui/public';
11+
import {
12+
IErrorObject,
13+
AlertsContextValue,
14+
AlertTypeParamsExpressionProps,
15+
} from '../../../../../../triggers_actions_ui/public';
1216
import { ES_GEO_FIELD_TYPES } from '../../types';
1317
import { GeoIndexPatternSelect } from '../util_components/geo_index_pattern_select';
1418
import { SingleFieldSelect } from '../util_components/single_field_select';
@@ -23,7 +27,7 @@ interface Props {
2327
errors: IErrorObject;
2428
setAlertParamsDate: (date: string) => void;
2529
setAlertParamsGeoField: (geoField: string) => void;
26-
setAlertProperty: (alertProp: string, alertParams: unknown) => void;
30+
setAlertProperty: AlertTypeParamsExpressionProps['setAlertProperty'];
2731
setIndexPattern: (indexPattern: IIndexPattern) => void;
2832
indexPattern: IIndexPattern;
2933
isInvalid: boolean;

0 commit comments

Comments
 (0)