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 (elastic#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.
  • Loading branch information
gmmorris committed Nov 20, 2020
1 parent eadb94b commit 16f9ccc
Show file tree
Hide file tree
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
Expand Up @@ -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 {
Expand Down
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
Expand Up @@ -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 {
Expand All @@ -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: {
Expand All @@ -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">
Expand All @@ -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
Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/alerts/common/alert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down Expand Up @@ -37,6 +37,7 @@ export interface AlertExecutionStatus {
}

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

export interface AlertAction {
group: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down
Loading

0 comments on commit 16f9ccc

Please sign in to comment.