[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.
This commit is contained in:
Gidi Meir Morris 2020-11-20 09:26:27 +00:00 committed by GitHub
parent 63cb5aee4e
commit 8aa7e13cb5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1061 additions and 115 deletions

View file

@ -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 {

View file

@ -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>
);
};

View file

@ -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 {

View file

@ -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;

View file

@ -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;

View file

@ -25,6 +25,7 @@ Table of Contents
- [GROUPED BY expression component](#grouped-by-expression-component)
- [FOR THE LAST expression component](#for-the-last-expression-component)
- [THRESHOLD expression component](#threshold-expression-component)
- [Alert Conditions Components](#alert-conditions-components)
- [Embed the Create Alert flyout within any Kibana plugin](#embed-the-create-alert-flyout-within-any-kibana-plugin)
- [Build and register Action Types](#build-and-register-action-types)
- [Built-in Action Types](#built-in-action-types)
@ -634,6 +635,155 @@ interface ThresholdExpressionProps {
|customComparators|(Optional) List of comparators that replaces the default options defined in constants `x-pack/plugins/triggers_actions_ui/public/common/constants/comparators.ts`.|
|popupPosition|(Optional) expression popup position. Default is `downLeft`. Recommend changing it for a small parent window space.|
## Alert Conditions Components
To aid in creating a uniform UX across Alert Types, we provide two components for specifying the conditions for detection of a certain alert under within any specific 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.
These can be used by any Alert Type to easily create the UI for adding action groups along with an Alert Type specific component.
For Example:
Given an Alert Type which requires different thresholds for each detected Action Group (for example), you might have a `ThresholdSpecifier` component for specifying the threshold for a specific Action Group.
```
const ThresholdSpecifier = (
{
actionGroup,
setThreshold
} : {
actionGroup?: ActionGroupWithCondition<number>;
setThreshold: (actionGroup: ActionGroupWithCondition<number>) => void;
}) => {
if (!actionGroup) {
// render empty if no condition action group is specified
return <Fragment />;
}
return (
<EuiFieldNumber
value={actionGroup.conditions}
onChange={(e) => {
const conditions = parseInt(e.target.value, 10);
if (e.target.value && !isNaN(conditions)) {
setThreshold({
...actionGroup,
conditions,
});
}
}}
/>
);
};
```
This component takes two props, one which is required (`actionGroup`) and one which is alert type specific (`setThreshold`).
The `actionGroup` will be populated by the `AlertConditions` component, but `setThreshold` will have to be provided by the AlertType itself.
To understand how this is used, lets take a closer look at `actionGroup`:
```
type ActionGroupWithCondition<T> = ActionGroup &
(
| // allow isRequired=false with or without conditions
{
conditions?: T;
isRequired?: false;
}
// but if isRequired=true then conditions must be specified
| {
conditions: T;
isRequired: true;
}
)
```
The `condition` field is Alert Type specific, and holds whichever type an Alert Type needs for specifying the condition under which a certain detection falls under that specific Action Group.
In our example, this is a `number` as that's all we need to speciufy the threshold which dictates whether an alert falls into one actio ngroup rather than another.
The `isRequired` field specifies whether this specific action group is _required_, that is, you can't reset its condition and _have_ to specify a some condition for it.
Using this `ThresholdSpecifier` component, we can now use `AlertConditionsGroup` & `AlertConditions` to enable the user to specify these thresholds for each action group in the alert type.
Like so:
```
interface ThresholdAlertTypeParams {
thresholds?: {
alert?: number;
warning?: number;
error?: number;
};
}
const DEFAULT_THRESHOLDS: ThresholdAlertTypeParams['threshold] = {
alert: 50,
warning: 80,
error: 90,
};
```
```
<AlertConditions
headline={'Set different thresholds for each level'}
actionGroups={[
{
id: 'alert',
name: 'Alert',
condition: DEFAULT_THRESHOLD
},
{
id: 'warning',
name: 'Warning',
},
{
id: 'error',
name: 'Error',
},
]}
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>
```
### The AlertConditions component
This component will render the `Conditions` header & headline, along with the selectors for adding every Action Group you specity.
Additionally it will clone its `children` for _each_ action group which has a `condition` specified for it, passing in the appropriate `actionGroup` prop for each one.
|Property|Description|
|---|---|
|headline|The headline title displayed above the fields |
|actionGroups|A list of `ActionGroupWithCondition` which includes all the action group you wish to offer the user and what conditions they are already configured to follow|
|onInitializeConditionsFor|A callback which is called when the user ask for a certain actionGroup to be initialized with an initial default condition. If you have no specific default, that's fine, as the component will render the action group's field even if the condition is empty (using a `null` or an `undefined`) and determines whether to render these fields by _the very presence_ of a `condition` field|
### The AlertConditionsGroup component
This component renders a standard EuiTitle foe each action group, wrapping the Alert Type specific component, in addition to a "reset" button which allows the user to reset the condition for that action group. The definition of what a _reset_ actually means is Alert Type specific, and up to the implementor to decide. In some case it might mean removing the condition, in others it might mean to reset it to some default value on the server side. In either case, it should _delete_ the `condition` field from the appropriate `actionGroup` as per the above example.
|Property|Description|
|---|---|
|onResetConditionsFor|A callback which is called when the user clicks the _reset_ button besides the action group's title. The implementor should use this to remove the `condition` from the specified actionGroup|
## Embed the Create Alert flyout within any Kibana plugin
Follow the instructions bellow to embed the Create Alert flyout within any Kibana plugin:

View file

@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Alert, AlertType } from '../../types';
import { AlertType } from '../../types';
import { InitialAlert } from '../sections/alert_form/alert_reducer';
/**
* NOTE: Applications that want to show the alerting UIs will need to add
@ -21,9 +22,9 @@ export const hasExecuteActionsCapability = (capabilities: Capabilities) =>
export const hasDeleteActionsCapability = (capabilities: Capabilities) =>
capabilities?.actions?.delete;
export function hasAllPrivilege(alert: Alert, alertType?: AlertType): boolean {
export function hasAllPrivilege(alert: InitialAlert, alertType?: AlertType): boolean {
return alertType?.authorizedConsumers[alert.consumer]?.all ?? false;
}
export function hasReadPrivilege(alert: Alert, alertType?: AlertType): boolean {
export function hasReadPrivilege(alert: InitialAlert, alertType?: AlertType): boolean {
return alertType?.authorizedConsumers[alert.consumer]?.read ?? false;
}

View file

@ -36,7 +36,7 @@ import { AddConnectorInline } from './connector_add_inline';
import { actionTypeCompare } from '../../lib/action_type_compare';
import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled';
import { VIEW_LICENSE_OPTIONS_LINK, DEFAULT_HIDDEN_ACTION_TYPES } from '../../../common/constants';
import { ActionGroup } from '../../../../../alerts/common';
import { ActionGroup, AlertActionParam } from '../../../../../alerts/common';
export interface ActionAccordionFormProps {
actions: AlertAction[];
@ -45,7 +45,7 @@ export interface ActionAccordionFormProps {
setActionIdByIndex: (id: string, index: number) => void;
setActionGroupIdByIndex?: (group: string, index: number) => void;
setAlertProperty: (actions: AlertAction[]) => void;
setActionParamsProperty: (key: string, value: any, index: number) => void;
setActionParamsProperty: (key: string, value: AlertActionParam, index: number) => void;
http: HttpSetup;
actionTypeRegistry: ActionTypeRegistryContract;
toastNotifications: ToastsSetup;

View file

@ -25,7 +25,7 @@ import {
EuiLoadingSpinner,
EuiBadge,
} from '@elastic/eui';
import { ResolvedActionGroup } from '../../../../../alerts/common';
import { AlertActionParam, ResolvedActionGroup } from '../../../../../alerts/common';
import {
IErrorObject,
AlertAction,
@ -50,7 +50,7 @@ export type ActionTypeFormProps = {
onAddConnector: () => void;
onConnectorSelected: (id: string) => void;
onDeleteAction: () => void;
setActionParamsProperty: (key: string, value: any, index: number) => void;
setActionParamsProperty: (key: string, value: AlertActionParam, index: number) => void;
actionTypesIndex: ActionTypeIndex;
connectors: ActionConnector[];
} & Pick<

View file

@ -75,8 +75,8 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
chrome,
} = useAppDependencies();
const [{}, dispatch] = useReducer(alertReducer, { alert });
const setInitialAlert = (key: string, value: any) => {
dispatch({ command: { type: 'setAlert' }, payload: { key, value } });
const setInitialAlert = (value: Alert) => {
dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } });
};
// Set breadcrumb and page title
@ -172,7 +172,7 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
<AlertEdit
initialAlert={alert}
onClose={() => {
setInitialAlert('alert', alert);
setInitialAlert(alert);
setEditFlyoutVisibility(false);
}}
/>

View file

@ -3,15 +3,14 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useCallback, useReducer, useState, useEffect } from 'react';
import { isObject } from 'lodash';
import React, { useCallback, useReducer, useMemo, useState, useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiTitle, EuiFlyoutHeader, EuiFlyout, EuiFlyoutBody, EuiPortal } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useAlertsContext } from '../../context/alerts_context';
import { Alert, AlertAction, IErrorObject } from '../../../types';
import { AlertForm, validateBaseProperties } from './alert_form';
import { alertReducer } from './alert_reducer';
import { AlertForm, isValidAlert, validateBaseProperties } from './alert_form';
import { alertReducer, InitialAlert, InitialAlertReducer } from './alert_reducer';
import { createAlert } from '../../lib/alert_api';
import { HealthCheck } from '../../components/health_check';
import { ConfirmAlertSave } from './confirm_alert_save';
@ -36,27 +35,32 @@ export const AlertAdd = ({
alertTypeId,
initialValues,
}: AlertAddProps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const initialAlert = ({
params: {},
consumer,
alertTypeId,
schedule: {
interval: '1m',
},
actions: [],
tags: [],
...(initialValues ? initialValues : {}),
} as unknown) as Alert;
const initialAlert: InitialAlert = useMemo(
() => ({
params: {},
consumer,
alertTypeId,
schedule: {
interval: '1m',
},
actions: [],
tags: [],
...(initialValues ? initialValues : {}),
}),
[alertTypeId, consumer, initialValues]
);
const [{ alert }, dispatch] = useReducer(alertReducer, { alert: initialAlert });
const [{ alert }, dispatch] = useReducer(alertReducer as InitialAlertReducer, {
alert: initialAlert,
});
const [isSaving, setIsSaving] = useState<boolean>(false);
const [isConfirmAlertSaveModalOpen, setIsConfirmAlertSaveModalOpen] = useState<boolean>(false);
const setAlert = (value: any) => {
const setAlert = (value: InitialAlert) => {
dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } });
};
const setAlertProperty = (key: string, value: any) => {
const setAlertProperty = <Key extends keyof Alert>(key: Key, value: Alert[Key] | null) => {
dispatch({ command: { type: 'setProperty' }, payload: { key, value } });
};
@ -73,7 +77,7 @@ export const AlertAdd = ({
const canShowActions = hasShowActionsCapability(capabilities);
useEffect(() => {
setAlertProperty('alertTypeId', alertTypeId);
setAlertProperty('alertTypeId', alertTypeId ?? null);
}, [alertTypeId]);
const closeFlyout = useCallback(() => {
@ -101,7 +105,7 @@ export const AlertAdd = ({
...(alertType ? alertType.validate(alert.params).errors : []),
...validateBaseProperties(alert).errors,
} as IErrorObject;
const hasErrors = parseErrors(errors);
const hasErrors = !isValidAlert(alert, errors);
const actionsErrors: Array<{
errors: IErrorObject;
@ -121,16 +125,18 @@ export const AlertAdd = ({
async function onSaveAlert(): Promise<Alert | undefined> {
try {
const newAlert = await createAlert({ http, alert });
toastNotifications.addSuccess(
i18n.translate('xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText', {
defaultMessage: 'Created alert "{alertName}"',
values: {
alertName: newAlert.name,
},
})
);
return newAlert;
if (isValidAlert(alert, errors)) {
const newAlert = await createAlert({ http, alert });
toastNotifications.addSuccess(
i18n.translate('xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText', {
defaultMessage: 'Created alert "{alertName}"',
values: {
alertName: newAlert.name,
},
})
);
return newAlert;
}
} catch (errorRes) {
toastNotifications.addDanger(
errorRes.body?.message ??
@ -207,11 +213,5 @@ export const AlertAdd = ({
);
};
const parseErrors: (errors: IErrorObject) => boolean = (errors) =>
!!Object.values(errors).find((errorList) => {
if (isObject(errorList)) return parseErrors(errorList as IErrorObject);
return errorList.length >= 1;
});
// eslint-disable-next-line import/no-default-export
export { AlertAdd as default };

View file

@ -0,0 +1,260 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as React from 'react';
import { mountWithIntl, nextTick } from '@kbn/test/jest';
import { act } from 'react-dom/test-utils';
import { ReactWrapper } from 'enzyme';
import { AlertConditions, ActionGroupWithCondition } from './alert_conditions';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiTitle,
EuiDescriptionList,
EuiDescriptionListTitle,
EuiDescriptionListDescription,
EuiButtonEmpty,
} from '@elastic/eui';
describe('alert_conditions', () => {
async function setup(element: React.ReactElement): Promise<ReactWrapper<unknown>> {
const wrapper = mountWithIntl(element);
// Wait for active space to resolve before requesting the component to update
await act(async () => {
await nextTick();
wrapper.update();
});
return wrapper;
}
it('renders with custom headline', async () => {
const wrapper = await setup(
<AlertConditions
headline={'Set different threshold with their own status'}
actionGroups={[]}
/>
);
expect(wrapper.find(EuiTitle).find(FormattedMessage).prop('id')).toMatchInlineSnapshot(
`"xpack.triggersActionsUI.sections.alertForm.conditions.title"`
);
expect(
wrapper.find(EuiTitle).find(FormattedMessage).prop('defaultMessage')
).toMatchInlineSnapshot(`"Conditions:"`);
expect(wrapper.find('[data-test-subj="alertConditionsHeadline"]').get(0))
.toMatchInlineSnapshot(`
<EuiText
color="subdued"
data-test-subj="alertConditionsHeadline"
size="s"
>
Set different threshold with their own status
</EuiText>
`);
});
it('renders any action group with conditions on it', async () => {
const ConditionForm = ({
actionGroup,
}: {
actionGroup?: ActionGroupWithCondition<{ someProp: string }>;
}) => {
return (
<EuiDescriptionList>
<EuiDescriptionListTitle>ID</EuiDescriptionListTitle>
<EuiDescriptionListDescription>{actionGroup?.id}</EuiDescriptionListDescription>
<EuiDescriptionListTitle>Name</EuiDescriptionListTitle>
<EuiDescriptionListDescription>{actionGroup?.name}</EuiDescriptionListDescription>
<EuiDescriptionListTitle>SomeProp</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{actionGroup?.conditions?.someProp}
</EuiDescriptionListDescription>
</EuiDescriptionList>
);
};
const wrapper = await setup(
<AlertConditions
actionGroups={[
{ id: 'default', name: 'Default', conditions: { someProp: 'my prop value' } },
]}
>
<ConditionForm />
</AlertConditions>
);
expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(0))
.toMatchInlineSnapshot(`
<EuiDescriptionListDescription>
default
</EuiDescriptionListDescription>
`);
expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(1))
.toMatchInlineSnapshot(`
<EuiDescriptionListDescription>
Default
</EuiDescriptionListDescription>
`);
expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(2))
.toMatchInlineSnapshot(`
<EuiDescriptionListDescription>
my prop value
</EuiDescriptionListDescription>
`);
});
it('doesnt render action group without conditions', async () => {
const ConditionForm = ({
actionGroup,
}: {
actionGroup?: ActionGroupWithCondition<{ someProp: string }>;
}) => {
return (
<EuiDescriptionList>
<EuiDescriptionListTitle>ID</EuiDescriptionListTitle>
<EuiDescriptionListDescription>{actionGroup?.id}</EuiDescriptionListDescription>
</EuiDescriptionList>
);
};
const wrapper = await setup(
<AlertConditions
actionGroups={[
{ id: 'default', name: 'Default', conditions: { someProp: 'default on a prop' } },
{
id: 'shouldRender',
name: 'Should Render',
conditions: { someProp: 'shouldRender on a prop' },
},
{
id: 'shouldntRender',
name: 'Should Not Render',
},
]}
>
<ConditionForm />
</AlertConditions>
);
expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(0))
.toMatchInlineSnapshot(`
<EuiDescriptionListDescription>
default
</EuiDescriptionListDescription>
`);
expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(1))
.toMatchInlineSnapshot(`
<EuiDescriptionListDescription>
shouldRender
</EuiDescriptionListDescription>
`);
expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).length).toEqual(2);
});
it('render add buttons for action group without conditions', async () => {
const onInitializeConditionsFor = jest.fn();
const ConditionForm = ({
actionGroup,
}: {
actionGroup?: ActionGroupWithCondition<{ someProp: string }>;
}) => {
return (
<EuiDescriptionList>
<EuiDescriptionListTitle>ID</EuiDescriptionListTitle>
<EuiDescriptionListDescription>{actionGroup?.id}</EuiDescriptionListDescription>
</EuiDescriptionList>
);
};
const wrapper = await setup(
<AlertConditions
actionGroups={[
{
id: 'shouldntRenderLink',
name: 'Should Not Render Link',
conditions: { someProp: 'shouldRender on a prop' },
},
{
id: 'shouldRenderLink',
name: 'Should Render A Link',
},
]}
onInitializeConditionsFor={onInitializeConditionsFor}
>
<ConditionForm />
</AlertConditions>
);
expect(wrapper.find(EuiButtonEmpty).get(0)).toMatchInlineSnapshot(`
<EuiButtonEmpty
flush="left"
onClick={[Function]}
size="s"
>
Should Render A Link
</EuiButtonEmpty>
`);
wrapper.find(EuiButtonEmpty).simulate('click');
expect(onInitializeConditionsFor).toHaveBeenCalledWith({
id: 'shouldRenderLink',
name: 'Should Render A Link',
});
});
it('passes in any additional props the container passes in', async () => {
const callbackProp = jest.fn();
const ConditionForm = ({
actionGroup,
someCallbackProp,
}: {
actionGroup?: ActionGroupWithCondition<{ someProp: string }>;
someCallbackProp: (actionGroup: ActionGroupWithCondition<{ someProp: string }>) => void;
}) => {
if (!actionGroup) {
return <div />;
}
// call callback when the actionGroup is available
someCallbackProp(actionGroup);
return (
<EuiDescriptionList>
<EuiDescriptionListTitle>ID</EuiDescriptionListTitle>
<EuiDescriptionListDescription>{actionGroup?.id}</EuiDescriptionListDescription>
<EuiDescriptionListTitle>Name</EuiDescriptionListTitle>
<EuiDescriptionListDescription>{actionGroup?.name}</EuiDescriptionListDescription>
<EuiDescriptionListTitle>SomeProp</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{actionGroup?.conditions?.someProp}
</EuiDescriptionListDescription>
</EuiDescriptionList>
);
};
await setup(
<AlertConditions
actionGroups={[
{ id: 'default', name: 'Default', conditions: { someProp: 'my prop value' } },
]}
>
<ConditionForm someCallbackProp={callbackProp} />
</AlertConditions>
);
expect(callbackProp).toHaveBeenCalledWith({
id: 'default',
name: 'Default',
conditions: { someProp: 'my prop value' },
});
});
});

View file

@ -0,0 +1,117 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { PropsWithChildren } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexItem, EuiText, EuiFlexGroup, EuiTitle, EuiButtonEmpty } from '@elastic/eui';
import { partition } from 'lodash';
import { ActionGroup, getBuiltinActionGroups } from '../../../../../alerts/common';
const BUILT_IN_ACTION_GROUPS: Set<string> = new Set(getBuiltinActionGroups().map(({ id }) => id));
export type ActionGroupWithCondition<T> = ActionGroup &
(
| // allow isRequired=false with or without conditions
{
conditions?: T;
isRequired?: false;
}
// but if isRequired=true then conditions must be specified
| {
conditions: T;
isRequired: true;
}
);
export interface AlertConditionsProps<ConditionProps> {
headline?: string;
actionGroups: Array<ActionGroupWithCondition<ConditionProps>>;
onInitializeConditionsFor?: (actionGroup: ActionGroupWithCondition<ConditionProps>) => void;
onResetConditionsFor?: (actionGroup: ActionGroupWithCondition<ConditionProps>) => void;
includeBuiltInActionGroups?: boolean;
}
export const AlertConditions = <ConditionProps extends any>({
headline,
actionGroups,
onInitializeConditionsFor,
onResetConditionsFor,
includeBuiltInActionGroups = false,
children,
}: PropsWithChildren<AlertConditionsProps<ConditionProps>>) => {
const [withConditions, withoutConditions] = partition(
includeBuiltInActionGroups
? actionGroups
: actionGroups.filter(({ id }) => !BUILT_IN_ACTION_GROUPS.has(id)),
(actionGroup) => actionGroup.hasOwnProperty('conditions')
);
return (
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<EuiTitle size="s">
<EuiFlexGroup component="span" alignItems="baseline">
<EuiFlexItem grow={false}>
<h6 className="alertConditions">
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.conditions.title"
defaultMessage="Conditions:"
/>
</h6>
</EuiFlexItem>
{headline && (
<EuiFlexItem>
<EuiText color="subdued" size="s" data-test-subj="alertConditionsHeadline">
{headline}
</EuiText>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup direction="column">
{withConditions.map((actionGroup) => (
<EuiFlexItem key={`condition-${actionGroup.id}`}>
{React.isValidElement(children) &&
React.cloneElement(
React.Children.only(children),
onResetConditionsFor
? {
actionGroup,
onResetConditionsFor,
}
: { actionGroup }
)}
</EuiFlexItem>
))}
{onInitializeConditionsFor && withoutConditions.length > 0 && (
<EuiFlexItem>
<EuiFlexGroup direction="row" alignItems="baseline">
<EuiFlexItem grow={false}>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.conditions.addConditionLabel"
defaultMessage="Add:"
/>
</EuiFlexItem>
{withoutConditions.map((actionGroup) => (
<EuiFlexItem key={`condition-add-${actionGroup.id}`} grow={false}>
<EuiButtonEmpty
flush="left"
size="s"
onClick={() => onInitializeConditionsFor(actionGroup)}
>
{actionGroup.name}
</EuiButtonEmpty>
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,98 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as React from 'react';
import { mountWithIntl, nextTick } from '@kbn/test/jest';
import { act } from 'react-dom/test-utils';
import { ReactWrapper } from 'enzyme';
import { AlertConditionsGroup } from './alert_conditions_group';
import { EuiFormRow, EuiButtonIcon } from '@elastic/eui';
describe('alert_conditions_group', () => {
async function setup(element: React.ReactElement): Promise<ReactWrapper<unknown>> {
const wrapper = mountWithIntl(element);
// Wait for active space to resolve before requesting the component to update
await act(async () => {
await nextTick();
wrapper.update();
});
return wrapper;
}
it('renders with actionGroup name as label', async () => {
const InnerComponent = () => <div>{'inner component'}</div>;
const wrapper = await setup(
<AlertConditionsGroup
actionGroup={{
id: 'myGroup',
name: 'My Group',
}}
>
<InnerComponent />
</AlertConditionsGroup>
);
expect(wrapper.find(EuiFormRow).prop('label')).toMatchInlineSnapshot(`
<EuiTitle
size="s"
>
<strong>
My Group
</strong>
</EuiTitle>
`);
expect(wrapper.find(InnerComponent).prop('actionGroup')).toMatchInlineSnapshot(`
Object {
"id": "myGroup",
"name": "My Group",
}
`);
});
it('renders a reset button when onResetConditionsFor is specified', async () => {
const onResetConditionsFor = jest.fn();
const wrapper = await setup(
<AlertConditionsGroup
actionGroup={{
id: 'myGroup',
name: 'My Group',
}}
onResetConditionsFor={onResetConditionsFor}
>
<div>{'inner component'}</div>
</AlertConditionsGroup>
);
expect(wrapper.find(EuiButtonIcon).prop('aria-label')).toMatchInlineSnapshot(`"Remove"`);
wrapper.find(EuiButtonIcon).simulate('click');
expect(onResetConditionsFor).toHaveBeenCalledWith({
id: 'myGroup',
name: 'My Group',
});
});
it('shouldnt render a reset button when isRequired is true', async () => {
const onResetConditionsFor = jest.fn();
const wrapper = await setup(
<AlertConditionsGroup
actionGroup={{
id: 'myGroup',
name: 'My Group',
conditions: true,
isRequired: true,
}}
onResetConditionsFor={onResetConditionsFor}
>
<div>{'inner component'}</div>
</AlertConditionsGroup>
);
expect(wrapper.find(EuiButtonIcon).length).toEqual(0);
});
});

View file

@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, PropsWithChildren } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFormRow, EuiButtonIcon, EuiTitle } from '@elastic/eui';
import { AlertConditionsProps, ActionGroupWithCondition } from './alert_conditions';
export type AlertConditionsGroupProps<ConditionProps> = {
actionGroup?: ActionGroupWithCondition<ConditionProps>;
} & Pick<AlertConditionsProps<ConditionProps>, 'onResetConditionsFor'>;
export const AlertConditionsGroup = <ConditionProps extends unknown>({
actionGroup,
onResetConditionsFor,
children,
...otherProps
}: PropsWithChildren<AlertConditionsGroupProps<ConditionProps>>) => {
if (!actionGroup) {
return null;
}
return (
<EuiFormRow
label={
<EuiTitle size="s">
<strong>{actionGroup.name}</strong>
</EuiTitle>
}
fullWidth
labelAppend={
onResetConditionsFor &&
!actionGroup.isRequired && (
<EuiButtonIcon
iconType="minusInCircle"
color="danger"
aria-label={i18n.translate(
'xpack.triggersActionsUI.sections.alertForm.conditions.removeConditionLabel',
{
defaultMessage: 'Remove',
}
)}
onClick={() => onResetConditionsFor(actionGroup)}
/>
)
}
>
{React.isValidElement(children) ? (
React.cloneElement(React.Children.only(children), {
actionGroup,
...otherProps,
})
) : (
<Fragment />
)}
</EuiFormRow>
);
};

View file

@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n';
import { useAlertsContext } from '../../context/alerts_context';
import { Alert, AlertAction, IErrorObject } from '../../../types';
import { AlertForm, validateBaseProperties } from './alert_form';
import { alertReducer } from './alert_reducer';
import { alertReducer, ConcreteAlertReducer } from './alert_reducer';
import { updateAlert } from '../../lib/alert_api';
import { HealthCheck } from '../../components/health_check';
import { HealthContextProvider } from '../../context/health_context';
@ -34,7 +34,9 @@ interface AlertEditProps {
}
export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => {
const [{ alert }, dispatch] = useReducer(alertReducer, { alert: initialAlert });
const [{ alert }, dispatch] = useReducer(alertReducer as ConcreteAlertReducer, {
alert: initialAlert,
});
const [isSaving, setIsSaving] = useState<boolean>(false);
const [hasActionsDisabled, setHasActionsDisabled] = useState<boolean>(false);
const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] = useState<boolean>(

View file

@ -33,14 +33,14 @@ import {
} from '@elastic/eui';
import { some, filter, map, fold } from 'fp-ts/lib/Option';
import { pipe } from 'fp-ts/lib/pipeable';
import { capitalize } from 'lodash';
import { capitalize, isObject } from 'lodash';
import { KibanaFeature } from '../../../../../features/public';
import {
getDurationNumberInItsUnit,
getDurationUnitValue,
} from '../../../../../alerts/common/parse_duration';
import { loadAlertTypes } from '../../lib/alert_api';
import { AlertReducerAction } from './alert_reducer';
import { AlertReducerAction, InitialAlert } from './alert_reducer';
import {
AlertTypeModel,
Alert,
@ -48,18 +48,19 @@ import {
AlertAction,
AlertTypeIndex,
AlertType,
ValidationResult,
} from '../../../types';
import { getTimeOptions } from '../../../common/lib/get_time_options';
import { useAlertsContext } from '../../context/alerts_context';
import { ActionForm } from '../action_connector_form';
import { ALERTS_FEATURE_ID } from '../../../../../alerts/common';
import { AlertActionParam, ALERTS_FEATURE_ID } from '../../../../../alerts/common';
import { hasAllPrivilege, hasShowActionsCapability } from '../../lib/capabilities';
import { SolutionFilter } from './solution_filter';
import './alert_form.scss';
const ENTER_KEY = 13;
export function validateBaseProperties(alertObject: Alert) {
export function validateBaseProperties(alertObject: InitialAlert): ValidationResult {
const validationResult = { errors: {} };
const errors = {
name: new Array<string>(),
@ -92,12 +93,25 @@ export function validateBaseProperties(alertObject: Alert) {
return validationResult;
}
const hasErrors: (errors: IErrorObject) => boolean = (errors) =>
!!Object.values(errors).find((errorList) => {
if (isObject(errorList)) return hasErrors(errorList as IErrorObject);
return errorList.length >= 1;
});
export function isValidAlert(
alertObject: InitialAlert | Alert,
validationResult: IErrorObject
): alertObject is Alert {
return !hasErrors(validationResult);
}
function getProducerFeatureName(producer: string, kibanaFeatures: KibanaFeature[]) {
return kibanaFeatures.find((featureItem) => featureItem.id === producer)?.name;
}
interface AlertFormProps {
alert: Alert;
alert: InitialAlert;
dispatch: React.Dispatch<AlertReducerAction>;
errors: IErrorObject;
canChangeTrigger?: boolean; // to hide Change trigger button
@ -203,10 +217,13 @@ export const AlertForm = ({
useEffect(() => {
setAlertTypeModel(alert.alertTypeId ? alertTypeRegistry.get(alert.alertTypeId) : null);
}, [alert, alertTypeRegistry]);
if (alert.alertTypeId && alertTypesIndex && alertTypesIndex.has(alert.alertTypeId)) {
setDefaultActionGroupId(alertTypesIndex.get(alert.alertTypeId)!.defaultActionGroupId);
}
}, [alert, alert.alertTypeId, alertTypesIndex, alertTypeRegistry]);
const setAlertProperty = useCallback(
(key: string, value: any) => {
<Key extends keyof Alert>(key: Key, value: Alert[Key] | null) => {
dispatch({ command: { type: 'setProperty' }, payload: { key, value } });
},
[dispatch]
@ -225,12 +242,16 @@ export const AlertForm = ({
dispatch({ command: { type: 'setScheduleProperty' }, payload: { key, value } });
};
const setActionProperty = (key: string, value: any, index: number) => {
const setActionProperty = <Key extends keyof AlertAction>(
key: Key,
value: AlertAction[Key] | null,
index: number
) => {
dispatch({ command: { type: 'setAlertActionProperty' }, payload: { key, value, index } });
};
const setActionParamsProperty = useCallback(
(key: string, value: any, index: number) => {
(key: string, value: AlertActionParam, index: number) => {
dispatch({ command: { type: 'setAlertActionParams' }, payload: { key, value, index } });
},
[dispatch]
@ -436,7 +457,10 @@ export const AlertForm = ({
</EuiFlexGroup>
)}
<EuiHorizontalRule />
{AlertParamsExpressionComponent ? (
{AlertParamsExpressionComponent &&
defaultActionGroupId &&
alert.alertTypeId &&
alertTypesIndex?.has(alert.alertTypeId) ? (
<Suspense fallback={<CenterJustifiedSpinner />}>
<AlertParamsExpressionComponent
alertParams={alert.params}
@ -446,12 +470,15 @@ export const AlertForm = ({
setAlertParams={setAlertParams}
setAlertProperty={setAlertProperty}
alertsContext={alertsContext}
defaultActionGroupId={defaultActionGroupId}
actionGroups={alertTypesIndex.get(alert.alertTypeId)!.actionGroups}
/>
</Suspense>
) : null}
{canShowActions &&
defaultActionGroupId &&
alertTypeModel &&
alert.alertTypeId &&
alertTypesIndex?.has(alert.alertTypeId) ? (
<ActionForm
actions={alert.actions}

View file

@ -3,38 +3,93 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObjectAttribute } from 'kibana/public';
import { isEqual } from 'lodash';
import { Reducer } from 'react';
import { AlertActionParam, IntervalSchedule } from '../../../../../alerts/common';
import { Alert, AlertAction } from '../../../types';
interface CommandType {
type:
export type InitialAlert = Partial<Alert> &
Pick<Alert, 'params' | 'consumer' | 'schedule' | 'actions' | 'tags'>;
interface CommandType<
T extends
| 'setAlert'
| 'setProperty'
| 'setScheduleProperty'
| 'setAlertParams'
| 'setAlertActionParams'
| 'setAlertActionProperty';
| 'setAlertActionProperty'
> {
type: T;
}
export interface AlertState {
alert: any;
alert: InitialAlert;
}
export interface AlertReducerAction {
command: CommandType;
payload: {
key: string;
value: {};
index?: number;
};
interface Payload<Keys, Value> {
key: Keys;
value: Value;
index?: number;
}
export const alertReducer = (state: any, action: AlertReducerAction) => {
const { command, payload } = action;
interface AlertPayload<Key extends keyof Alert> {
key: Key;
value: Alert[Key] | null;
index?: number;
}
interface AlertActionPayload<Key extends keyof AlertAction> {
key: Key;
value: AlertAction[Key] | null;
index?: number;
}
interface AlertSchedulePayload<Key extends keyof IntervalSchedule> {
key: Key;
value: IntervalSchedule[Key];
index?: number;
}
export type AlertReducerAction =
| {
command: CommandType<'setAlert'>;
payload: Payload<'alert', InitialAlert>;
}
| {
command: CommandType<'setProperty'>;
payload: AlertPayload<keyof Alert>;
}
| {
command: CommandType<'setScheduleProperty'>;
payload: AlertSchedulePayload<keyof IntervalSchedule>;
}
| {
command: CommandType<'setAlertParams'>;
payload: Payload<string, unknown>;
}
| {
command: CommandType<'setAlertActionParams'>;
payload: Payload<string, AlertActionParam>;
}
| {
command: CommandType<'setAlertActionProperty'>;
payload: AlertActionPayload<keyof AlertAction>;
};
export type InitialAlertReducer = Reducer<{ alert: InitialAlert }, AlertReducerAction>;
export type ConcreteAlertReducer = Reducer<{ alert: Alert }, AlertReducerAction>;
export const alertReducer = <AlertPhase extends InitialAlert | Alert>(
state: { alert: AlertPhase },
action: AlertReducerAction
) => {
const { alert } = state;
switch (command.type) {
switch (action.command.type) {
case 'setAlert': {
const { key, value } = payload;
const { key, value } = action.payload as Payload<'alert', AlertPhase>;
if (key === 'alert') {
return {
...state,
@ -45,7 +100,7 @@ export const alertReducer = (state: any, action: AlertReducerAction) => {
}
}
case 'setProperty': {
const { key, value } = payload;
const { key, value } = action.payload as AlertPayload<keyof Alert>;
if (isEqual(alert[key], value)) {
return state;
} else {
@ -59,8 +114,8 @@ export const alertReducer = (state: any, action: AlertReducerAction) => {
}
}
case 'setScheduleProperty': {
const { key, value } = payload;
if (isEqual(alert.schedule[key], value)) {
const { key, value } = action.payload as AlertSchedulePayload<keyof IntervalSchedule>;
if (alert.schedule && isEqual(alert.schedule[key], value)) {
return state;
} else {
return {
@ -76,7 +131,7 @@ export const alertReducer = (state: any, action: AlertReducerAction) => {
}
}
case 'setAlertParams': {
const { key, value } = payload;
const { key, value } = action.payload as Payload<string, Record<string, unknown>>;
if (isEqual(alert.params[key], value)) {
return state;
} else {
@ -93,7 +148,10 @@ export const alertReducer = (state: any, action: AlertReducerAction) => {
}
}
case 'setAlertActionParams': {
const { key, value, index } = payload;
const { key, value, index } = action.payload as Payload<
keyof AlertAction,
SavedObjectAttribute
>;
if (index === undefined || isEqual(alert.actions[index][key], value)) {
return state;
} else {
@ -116,7 +174,7 @@ export const alertReducer = (state: any, action: AlertReducerAction) => {
}
}
case 'setAlertActionProperty': {
const { key, value, index } = payload;
const { key, value, index } = action.payload as AlertActionPayload<keyof AlertAction>;
if (index === undefined || isEqual(alert.actions[index][key], value)) {
return state;
} else {

View file

@ -5,6 +5,12 @@
*/
import { lazy } from 'react';
import { suspendedComponentWithProps } from '../../lib/suspended_component_with_props';
export {
AlertConditions,
ActionGroupWithCondition,
AlertConditionsProps,
} from './alert_conditions';
export { AlertConditionsGroup } from './alert_conditions_group';
export const AlertAdd = suspendedComponentWithProps(lazy(() => import('./alert_add')));
export const AlertEdit = suspendedComponentWithProps(lazy(() => import('./alert_edit')));

View file

@ -6,6 +6,12 @@
import { lazy } from 'react';
import { suspendedComponentWithProps } from '../lib/suspended_component_with_props';
export {
ActionGroupWithCondition,
AlertConditionsProps,
AlertConditions,
AlertConditionsGroup,
} from './alert_form';
export const AlertAdd = suspendedComponentWithProps(lazy(() => import('./alert_form/alert_add')));
export const AlertEdit = suspendedComponentWithProps(lazy(() => import('./alert_form/alert_edit')));

View file

@ -9,7 +9,12 @@ import { Plugin } from './plugin';
export { AlertsContextProvider, AlertsContextValue } from './application/context/alerts_context';
export { ActionsConnectorsContextProvider } from './application/context/actions_connectors_context';
export { AlertAdd } from './application/sections/alert_form';
export { AlertEdit } from './application/sections';
export {
AlertEdit,
AlertConditions,
AlertConditionsGroup,
ActionGroupWithCondition,
} from './application/sections';
export { ActionForm } from './application/sections/action_connector_form';
export {
AlertAction,

View file

@ -6,7 +6,7 @@
import type { PublicMethodsOf } from '@kbn/utility-types';
import type { HttpSetup, DocLinksStart, ToastsSetup } from 'kibana/public';
import { ComponentType } from 'react';
import { ActionGroup } from '../../alerts/common';
import { ActionGroup, AlertActionParam } from '../../alerts/common';
import { ActionType } from '../../actions/common';
import { TypeRegistry } from './application/type_registry';
import {
@ -52,7 +52,7 @@ export interface ActionConnectorFieldsProps<TActionConnector> {
export interface ActionParamsProps<TParams> {
actionParams: TParams;
index: number;
editAction: (property: string, value: any, index: number) => void;
editAction: (key: string, value: AlertActionParam, index: number) => void;
errors: IErrorObject;
messageVariables?: ActionVariable[];
defaultMessage?: string;
@ -166,9 +166,11 @@ export interface AlertTypeParamsExpressionProps<
alertInterval: string;
alertThrottle: string;
setAlertParams: (property: string, value: any) => void;
setAlertProperty: (key: string, value: any) => void;
setAlertProperty: <Key extends keyof Alert>(key: Key, value: Alert[Key] | null) => void;
errors: IErrorObject;
alertsContext: AlertsContextValue;
defaultActionGroupId: string;
actionGroups: ActionGroup[];
}
export interface AlertTypeModel<AlertParamsType = any, AlertsContextValue = any> {