diff --git a/x-pack/examples/alerting_example/common/constants.ts b/x-pack/examples/alerting_example/common/constants.ts index dd9cc21954e61..40cc298db795a 100644 --- a/x-pack/examples/alerting_example/common/constants.ts +++ b/x-pack/examples/alerting_example/common/constants.ts @@ -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 { diff --git a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx index a5d158fca836b..abbe1d2a48d11 100644 --- a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx +++ b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx @@ -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 = ({ - 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> = ({ 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 ( @@ -67,6 +100,88 @@ export const AlwaysFiringExpression: React.FunctionComponent + + + + { + setAlertParams('thresholds', { + ...thresholds, + ...pick(DEFAULT_THRESHOLDS, actionGroup.id), + }); + }} + > + { + setAlertParams('thresholds', omit(thresholds, actionGroup.id)); + }} + > + { + setAlertParams('thresholds', { + ...thresholds, + [actionGroup.id]: actionGroup.conditions, + }); + }} + /> + + + + + ); }; + +interface TShirtSelectorProps { + actionGroup?: ActionGroupWithCondition; + setTShirtThreshold: (actionGroup: ActionGroupWithCondition) => void; +} +const TShirtSelector = ({ actionGroup, setTShirtThreshold }: TShirtSelectorProps) => { + const [isOpen, setIsOpen] = useState(false); + + if (!actionGroup) { + return null; + } + + return ( + setIsOpen(true)} + /> + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + ownFocus + anchorPosition="downLeft" + > + + + {'Is Above'} + + + { + const conditions = parseInt(e.target.value, 10); + if (e.target.value && !isNaN(conditions)) { + setTShirtThreshold({ + ...actionGroup, + conditions, + }); + } + }} + /> + + + + ); +}; diff --git a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts index d02406a23045e..1900f55a51a55 100644 --- a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts +++ b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts @@ -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 = { 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 { diff --git a/x-pack/plugins/alerts/common/alert.ts b/x-pack/plugins/alerts/common/alert.ts index 97a9a58400e38..88f6090d20737 100644 --- a/x-pack/plugins/alerts/common/alert.ts +++ b/x-pack/plugins/alerts/common/alert.ts @@ -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; @@ -37,6 +37,7 @@ export interface AlertExecutionStatus { } export type AlertActionParams = SavedObjectAttributes; +export type AlertActionParam = SavedObjectAttribute; export interface AlertAction { group: string; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx index e5e43210d1e6b..0a722734ffc5a 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx @@ -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; diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index ef81065608ad4..3e5e95996c80f 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -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; + setThreshold: (actionGroup: ActionGroupWithCondition) => void; +}) => { + if (!actionGroup) { + // render empty if no condition action group is specified + return ; + } + + return ( + { + 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 = 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, +}; +``` + +``` + { + setAlertParams('thresholds', { + ...thresholds, + ...pick(DEFAULT_THRESHOLDS, actionGroup.id), + }); + }} +> + { + setAlertParams('thresholds', omit(thresholds, actionGroup.id)); + }} + > + { + setAlertParams('thresholds', { + ...thresholds, + [actionGroup.id]: actionGroup.conditions, + }); + }} + /> + + +``` + +### 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: diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts index 9e89a38377a4d..7fb50eaab7d7d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts @@ -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; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 50f5167b9e5c2..83e6386122eb2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -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; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx index bd40d35b15b2d..5f1798d101d94 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx @@ -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< diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index b38f0e749a28d..d7de7e0a82c1e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -75,8 +75,8 @@ export const AlertDetails: React.FunctionComponent = ({ 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 = ({ { - setInitialAlert('alert', alert); + setInitialAlert(alert); setEditFlyoutVisibility(false); }} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index 741cbadb07070..34a4c909c65a9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -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 [{ alert }, dispatch] = useReducer(alertReducer, { alert: initialAlert }); + const initialAlert: InitialAlert = useMemo( + () => ({ + params: {}, + consumer, + alertTypeId, + schedule: { + interval: '1m', + }, + actions: [], + tags: [], + ...(initialValues ? initialValues : {}), + }), + [alertTypeId, consumer, initialValues] + ); + + const [{ alert }, dispatch] = useReducer(alertReducer as InitialAlertReducer, { + alert: initialAlert, + }); const [isSaving, setIsSaving] = useState(false); const [isConfirmAlertSaveModalOpen, setIsConfirmAlertSaveModalOpen] = useState(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: 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 { 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 }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx new file mode 100644 index 0000000000000..8029b43a2cf53 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx @@ -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> { + 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( + + ); + + 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(` + + Set different threshold with their own status + + `); + }); + + it('renders any action group with conditions on it', async () => { + const ConditionForm = ({ + actionGroup, + }: { + actionGroup?: ActionGroupWithCondition<{ someProp: string }>; + }) => { + return ( + + ID + {actionGroup?.id} + Name + {actionGroup?.name} + SomeProp + + {actionGroup?.conditions?.someProp} + + + ); + }; + + const wrapper = await setup( + + + + ); + + expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(0)) + .toMatchInlineSnapshot(` + + default + + `); + + expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(1)) + .toMatchInlineSnapshot(` + + Default + + `); + + expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(2)) + .toMatchInlineSnapshot(` + + my prop value + + `); + }); + + it('doesnt render action group without conditions', async () => { + const ConditionForm = ({ + actionGroup, + }: { + actionGroup?: ActionGroupWithCondition<{ someProp: string }>; + }) => { + return ( + + ID + {actionGroup?.id} + + ); + }; + + const wrapper = await setup( + + + + ); + + expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(0)) + .toMatchInlineSnapshot(` + + default + + `); + + expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(1)) + .toMatchInlineSnapshot(` + + shouldRender + + `); + + 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 ( + + ID + {actionGroup?.id} + + ); + }; + + const wrapper = await setup( + + + + ); + + expect(wrapper.find(EuiButtonEmpty).get(0)).toMatchInlineSnapshot(` + + Should Render A Link + + `); + 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
; + } + + // call callback when the actionGroup is available + someCallbackProp(actionGroup); + return ( + + ID + {actionGroup?.id} + Name + {actionGroup?.name} + SomeProp + + {actionGroup?.conditions?.someProp} + + + ); + }; + + await setup( + + + + ); + + expect(callbackProp).toHaveBeenCalledWith({ + id: 'default', + name: 'Default', + conditions: { someProp: 'my prop value' }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.tsx new file mode 100644 index 0000000000000..1eb086dd6a2c5 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.tsx @@ -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 = new Set(getBuiltinActionGroups().map(({ id }) => id)); + +export type ActionGroupWithCondition = 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 { + headline?: string; + actionGroups: Array>; + onInitializeConditionsFor?: (actionGroup: ActionGroupWithCondition) => void; + onResetConditionsFor?: (actionGroup: ActionGroupWithCondition) => void; + includeBuiltInActionGroups?: boolean; +} + +export const AlertConditions = ({ + headline, + actionGroups, + onInitializeConditionsFor, + onResetConditionsFor, + includeBuiltInActionGroups = false, + children, +}: PropsWithChildren>) => { + const [withConditions, withoutConditions] = partition( + includeBuiltInActionGroups + ? actionGroups + : actionGroups.filter(({ id }) => !BUILT_IN_ACTION_GROUPS.has(id)), + (actionGroup) => actionGroup.hasOwnProperty('conditions') + ); + + return ( + + + + + +
+ +
+
+ {headline && ( + + + {headline} + + + )} +
+
+
+ + + {withConditions.map((actionGroup) => ( + + {React.isValidElement(children) && + React.cloneElement( + React.Children.only(children), + onResetConditionsFor + ? { + actionGroup, + onResetConditionsFor, + } + : { actionGroup } + )} + + ))} + {onInitializeConditionsFor && withoutConditions.length > 0 && ( + + + + + + {withoutConditions.map((actionGroup) => ( + + onInitializeConditionsFor(actionGroup)} + > + {actionGroup.name} + + + ))} + + + )} + + +
+ ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.test.tsx new file mode 100644 index 0000000000000..dd12af4ae9e62 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.test.tsx @@ -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> { + 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 = () =>
{'inner component'}
; + const wrapper = await setup( + + + + ); + + expect(wrapper.find(EuiFormRow).prop('label')).toMatchInlineSnapshot(` + + + My Group + + + `); + 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( + +
{'inner component'}
+
+ ); + + 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( + +
{'inner component'}
+
+ ); + + expect(wrapper.find(EuiButtonIcon).length).toEqual(0); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx new file mode 100644 index 0000000000000..879f276317503 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx @@ -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 = { + actionGroup?: ActionGroupWithCondition; +} & Pick, 'onResetConditionsFor'>; + +export const AlertConditionsGroup = ({ + actionGroup, + onResetConditionsFor, + children, + ...otherProps +}: PropsWithChildren>) => { + if (!actionGroup) { + return null; + } + + return ( + + {actionGroup.name} + + } + fullWidth + labelAppend={ + onResetConditionsFor && + !actionGroup.isRequired && ( + onResetConditionsFor(actionGroup)} + /> + ) + } + > + {React.isValidElement(children) ? ( + React.cloneElement(React.Children.only(children), { + actionGroup, + ...otherProps, + }) + ) : ( + + )} + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index d5ae701546c64..2e2a77fa6afc3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -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(false); const [hasActionsDisabled, setHasActionsDisabled] = useState(false); const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] = useState( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 41d68156b4333..85939fc079e9f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -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(), @@ -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; 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: 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: 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 = ({ )} - {AlertParamsExpressionComponent ? ( + {AlertParamsExpressionComponent && + defaultActionGroupId && + alert.alertTypeId && + alertTypesIndex?.has(alert.alertTypeId) ? ( }> ) : null} {canShowActions && defaultActionGroupId && alertTypeModel && + alert.alertTypeId && alertTypesIndex?.has(alert.alertTypeId) ? ( & + Pick; + +interface CommandType< + T extends | 'setAlert' | 'setProperty' | 'setScheduleProperty' | 'setAlertParams' | 'setAlertActionParams' - | 'setAlertActionProperty'; + | 'setAlertActionProperty' +> { + type: T; } export interface AlertState { - alert: any; + alert: InitialAlert; +} + +interface Payload { + key: Keys; + value: Value; + index?: number; +} + +interface AlertPayload { + key: Key; + value: Alert[Key] | null; + index?: number; +} + +interface AlertActionPayload { + key: Key; + value: AlertAction[Key] | null; + index?: number; } -export interface AlertReducerAction { - command: CommandType; - payload: { - key: string; - value: {}; - index?: number; - }; +interface AlertSchedulePayload { + key: Key; + value: IntervalSchedule[Key]; + index?: number; } -export const alertReducer = (state: any, action: AlertReducerAction) => { - const { command, payload } = action; +export type AlertReducerAction = + | { + command: CommandType<'setAlert'>; + payload: Payload<'alert', InitialAlert>; + } + | { + command: CommandType<'setProperty'>; + payload: AlertPayload; + } + | { + command: CommandType<'setScheduleProperty'>; + payload: AlertSchedulePayload; + } + | { + command: CommandType<'setAlertParams'>; + payload: Payload; + } + | { + command: CommandType<'setAlertActionParams'>; + payload: Payload; + } + | { + command: CommandType<'setAlertActionProperty'>; + payload: AlertActionPayload; + }; + +export type InitialAlertReducer = Reducer<{ alert: InitialAlert }, AlertReducerAction>; +export type ConcreteAlertReducer = Reducer<{ alert: Alert }, AlertReducerAction>; + +export const alertReducer = ( + 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; 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; + 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>; 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; if (index === undefined || isEqual(alert.actions[index][key], value)) { return state; } else { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx index 79720edc4672e..421f0fc26dd68 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx @@ -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'))); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx index 677ee139271c0..490aeb5be8bd3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx @@ -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'))); diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index c479359ff7e6e..025741aa7f9bd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -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, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 16c6bbc215ddc..cc0522eeb52a1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -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 { export interface ActionParamsProps { 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: Key, value: Alert[Key] | null) => void; errors: IErrorObject; alertsContext: AlertsContextValue; + defaultActionGroupId: string; + actionGroups: ActionGroup[]; } export interface AlertTypeModel {