Skip to content

Commit

Permalink
[Alerting] Adds generic UI for the definition of conditions for Actio…
Browse files Browse the repository at this point in the history
…n Groups (#83278) (#83896)

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.
gmmorris authored Nov 20, 2020

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 5656b3a commit addf1a6
Showing 22 changed files with 1,062 additions and 116 deletions.
9 changes: 9 additions & 0 deletions x-pack/examples/alerting_example/common/constants.ts
Original file line number Diff line number Diff line change
@@ -8,6 +8,15 @@ export const ALERTING_EXAMPLE_APP_ID = 'AlertingExample';

// always firing
export const DEFAULT_INSTANCES_TO_GENERATE = 5;
export interface AlwaysFiringParams {
instances?: number;
thresholds?: {
small?: number;
medium?: number;
large?: number;
};
}
export type AlwaysFiringActionGroupIds = keyof AlwaysFiringParams['thresholds'];

// Astros
export enum Craft {
147 changes: 131 additions & 16 deletions x-pack/examples/alerting_example/public/alert_types/always_firing.tsx
Original file line number Diff line number Diff line change
@@ -4,17 +4,31 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { Fragment } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiFieldNumber, EuiFormRow } from '@elastic/eui';
import React, { Fragment, useState } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFieldNumber,
EuiFormRow,
EuiPopover,
EuiExpression,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { AlertTypeModel } from '../../../../plugins/triggers_actions_ui/public';
import { DEFAULT_INSTANCES_TO_GENERATE } from '../../common/constants';

interface AlwaysFiringParamsProps {
alertParams: { instances?: number };
setAlertParams: (property: string, value: any) => void;
errors: { [key: string]: string[] };
}
import { omit, pick } from 'lodash';
import {
ActionGroupWithCondition,
AlertConditions,
AlertConditionsGroup,
AlertTypeModel,
AlertTypeParamsExpressionProps,
AlertsContextValue,
} from '../../../../plugins/triggers_actions_ui/public';
import {
AlwaysFiringParams,
AlwaysFiringActionGroupIds,
DEFAULT_INSTANCES_TO_GENERATE,
} from '../../common/constants';

export function getAlertType(): AlertTypeModel {
return {
@@ -24,7 +38,7 @@ export function getAlertType(): AlertTypeModel {
iconClass: 'bolt',
documentationUrl: null,
alertParamsExpression: AlwaysFiringExpression,
validate: (alertParams: AlwaysFiringParamsProps['alertParams']) => {
validate: (alertParams: AlwaysFiringParams) => {
const { instances } = alertParams;
const validationResult = {
errors: {
@@ -44,11 +58,30 @@ export function getAlertType(): AlertTypeModel {
};
}

export const AlwaysFiringExpression: React.FunctionComponent<AlwaysFiringParamsProps> = ({
alertParams,
setAlertParams,
}) => {
const { instances = DEFAULT_INSTANCES_TO_GENERATE } = alertParams;
const DEFAULT_THRESHOLDS: AlwaysFiringParams['thresholds'] = {
small: 0,
medium: 5000,
large: 10000,
};

export const AlwaysFiringExpression: React.FunctionComponent<AlertTypeParamsExpressionProps<
AlwaysFiringParams,
AlertsContextValue
>> = ({ alertParams, setAlertParams, actionGroups, defaultActionGroupId }) => {
const {
instances = DEFAULT_INSTANCES_TO_GENERATE,
thresholds = pick(DEFAULT_THRESHOLDS, defaultActionGroupId),
} = alertParams;

const actionGroupsWithConditions = actionGroups.map((actionGroup) =>
Number.isInteger(thresholds[actionGroup.id as AlwaysFiringActionGroupIds])
? {
...actionGroup,
conditions: thresholds[actionGroup.id as AlwaysFiringActionGroupIds]!,
}
: actionGroup
);

return (
<Fragment>
<EuiFlexGroup gutterSize="s" wrap direction="column">
@@ -67,6 +100,88 @@ export const AlwaysFiringExpression: React.FunctionComponent<AlwaysFiringParamsP
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiFlexGroup>
<EuiFlexItem grow={true}>
<AlertConditions
headline={'Set different thresholds for randomly generated T-Shirt sizes'}
actionGroups={actionGroupsWithConditions}
onInitializeConditionsFor={(actionGroup) => {
setAlertParams('thresholds', {
...thresholds,
...pick(DEFAULT_THRESHOLDS, actionGroup.id),
});
}}
>
<AlertConditionsGroup
onResetConditionsFor={(actionGroup) => {
setAlertParams('thresholds', omit(thresholds, actionGroup.id));
}}
>
<TShirtSelector
setTShirtThreshold={(actionGroup) => {
setAlertParams('thresholds', {
...thresholds,
[actionGroup.id]: actionGroup.conditions,
});
}}
/>
</AlertConditionsGroup>
</AlertConditions>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
</Fragment>
);
};

interface TShirtSelectorProps {
actionGroup?: ActionGroupWithCondition<number>;
setTShirtThreshold: (actionGroup: ActionGroupWithCondition<number>) => void;
}
const TShirtSelector = ({ actionGroup, setTShirtThreshold }: TShirtSelectorProps) => {
const [isOpen, setIsOpen] = useState(false);

if (!actionGroup) {
return null;
}

return (
<EuiPopover
panelPaddingSize="s"
button={
<EuiExpression
description={'Is Above'}
value={actionGroup.conditions}
isActive={isOpen}
onClick={() => setIsOpen(true)}
/>
}
isOpen={isOpen}
closePopover={() => setIsOpen(false)}
ownFocus
anchorPosition="downLeft"
>
<EuiFlexGroup>
<EuiFlexItem grow={false} style={{ width: 150 }}>
{'Is Above'}
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ width: 100 }}>
<EuiFieldNumber
compressed
value={actionGroup.conditions}
onChange={(e) => {
const conditions = parseInt(e.target.value, 10);
if (e.target.value && !isNaN(conditions)) {
setTShirtThreshold({
...actionGroup,
conditions,
});
}
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPopover>
);
};
Original file line number Diff line number Diff line change
@@ -5,31 +5,56 @@
*/

import uuid from 'uuid';
import { range, random } from 'lodash';
import { range } from 'lodash';
import { AlertType } from '../../../../plugins/alerts/server';
import { DEFAULT_INSTANCES_TO_GENERATE, ALERTING_EXAMPLE_APP_ID } from '../../common/constants';
import {
DEFAULT_INSTANCES_TO_GENERATE,
ALERTING_EXAMPLE_APP_ID,
AlwaysFiringParams,
} from '../../common/constants';

const ACTION_GROUPS = [
{ id: 'small', name: 'small' },
{ id: 'medium', name: 'medium' },
{ id: 'large', name: 'large' },
{ id: 'small', name: 'Small t-shirt' },
{ id: 'medium', name: 'Medium t-shirt' },
{ id: 'large', name: 'Large t-shirt' },
];
const DEFAULT_ACTION_GROUP = 'small';

export const alertType: AlertType = {
function getTShirtSizeByIdAndThreshold(id: string, thresholds: AlwaysFiringParams['thresholds']) {
const idAsNumber = parseInt(id, 10);
if (!isNaN(idAsNumber)) {
if (thresholds?.large && thresholds.large < idAsNumber) {
return 'large';
}
if (thresholds?.medium && thresholds.medium < idAsNumber) {
return 'medium';
}
if (thresholds?.small && thresholds.small < idAsNumber) {
return 'small';
}
}
return DEFAULT_ACTION_GROUP;
}

export const alertType: AlertType<AlwaysFiringParams> = {
id: 'example.always-firing',
name: 'Always firing',
actionGroups: ACTION_GROUPS,
defaultActionGroupId: 'small',
async executor({ services, params: { instances = DEFAULT_INSTANCES_TO_GENERATE }, state }) {
defaultActionGroupId: DEFAULT_ACTION_GROUP,
async executor({
services,
params: { instances = DEFAULT_INSTANCES_TO_GENERATE, thresholds },
state,
}) {
const count = (state.count ?? 0) + 1;

range(instances)
.map(() => ({ id: uuid.v4(), tshirtSize: ACTION_GROUPS[random(0, 2)].id! }))
.forEach((instance: { id: string; tshirtSize: string }) => {
.map(() => uuid.v4())
.forEach((id: string) => {
services
.alertInstanceFactory(instance.id)
.alertInstanceFactory(id)
.replaceState({ triggerdOnCycle: count })
.scheduleActions(instance.tshirtSize);
.scheduleActions(getTShirtSizeByIdAndThreshold(id, thresholds));
});

return {
3 changes: 2 additions & 1 deletion x-pack/plugins/alerts/common/alert.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { SavedObjectAttributes } from 'kibana/server';
import { SavedObjectAttribute, SavedObjectAttributes } from 'kibana/server';

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

export type AlertActionParams = SavedObjectAttributes;
export type AlertActionParam = SavedObjectAttribute;

export interface AlertAction {
group: string;
Original file line number Diff line number Diff line change
@@ -8,7 +8,11 @@ import React, { Fragment, FunctionComponent, useEffect, useRef } from 'react';
import { EuiFormRow } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { IErrorObject, AlertsContextValue } from '../../../../../../triggers_actions_ui/public';
import {
IErrorObject,
AlertsContextValue,
AlertTypeParamsExpressionProps,
} from '../../../../../../triggers_actions_ui/public';
import { ES_GEO_FIELD_TYPES } from '../../types';
import { GeoIndexPatternSelect } from '../util_components/geo_index_pattern_select';
import { SingleFieldSelect } from '../util_components/single_field_select';
@@ -23,7 +27,7 @@ interface Props {
errors: IErrorObject;
setAlertParamsDate: (date: string) => void;
setAlertParamsGeoField: (geoField: string) => void;
setAlertProperty: (alertProp: string, alertParams: unknown) => void;
setAlertProperty: AlertTypeParamsExpressionProps['setAlertProperty'];
setIndexPattern: (indexPattern: IIndexPattern) => void;
indexPattern: IIndexPattern;
isInvalid: boolean;
Loading

0 comments on commit addf1a6

Please sign in to comment.