From 6e44558d80939eb57c70d1cddf605aa1db57b3a2 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Tue, 10 Nov 2020 15:51:33 -0800 Subject: [PATCH 1/3] Added ability to assign alert actions to resolved action group in UI --- .../alerts/common/builtin_action_groups.ts | 18 +++++++ .../email/email_params.tsx | 12 ++++- .../server_log/server_log_params.tsx | 16 +++++- .../slack/slack_params.tsx | 12 ++++- .../public/application/constants/index.ts | 9 ++++ .../application/lib/action_variables.ts | 12 +++-- .../action_connector_form/action_form.tsx | 4 +- .../action_type_form.tsx | 50 +++++++++++++++++-- .../sections/alert_form/alert_form.tsx | 5 +- .../triggers_actions_ui/public/types.ts | 2 +- 10 files changed, 118 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/alerts/common/builtin_action_groups.ts diff --git a/x-pack/plugins/alerts/common/builtin_action_groups.ts b/x-pack/plugins/alerts/common/builtin_action_groups.ts new file mode 100644 index 0000000000000..d31f75357d370 --- /dev/null +++ b/x-pack/plugins/alerts/common/builtin_action_groups.ts @@ -0,0 +1,18 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { ActionGroup } from './alert_type'; + +export const ResolvedActionGroup: ActionGroup = { + id: 'resolved', + name: i18n.translate('xpack.alerts.builtinActionGroups.resolved', { + defaultMessage: 'Resolved', + }), +}; + +export function getBuiltinActionGroups(): ActionGroup[] { + return [ResolvedActionGroup]; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx index a91cf3e7552bc..eacdf20747fdc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx @@ -11,6 +11,7 @@ import { ActionParamsProps } from '../../../../types'; import { EmailActionParams } from '../types'; import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; +import { resolvedActionGroupMessage } from '../../../constants'; export const EmailParamsFields = ({ actionParams, @@ -28,11 +29,18 @@ export const EmailParamsFields = ({ const [addBCC, setAddBCC] = useState(false); useEffect(() => { - if (!message && defaultMessage && defaultMessage.length > 0) { + if (defaultMessage === resolvedActionGroupMessage) { + editAction('message', defaultMessage, index); + } else if ( + (!message || message === resolvedActionGroupMessage) && + defaultMessage && + defaultMessage.length > 0 + ) { editAction('message', defaultMessage, index); } + // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [defaultMessage]); return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx index b79fa0ea94050..a3619f96a45b2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx @@ -9,6 +9,7 @@ import { EuiSelect, EuiFormRow } from '@elastic/eui'; import { ActionParamsProps } from '../../../../types'; import { ServerLogActionParams } from '.././types'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; +import { resolvedActionGroupMessage } from '../../../constants'; export const ServerLogParamsFields: React.FunctionComponent { editAction('level', 'info', index); - if (!message && defaultMessage && defaultMessage.length > 0) { + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (defaultMessage === resolvedActionGroupMessage) { + editAction('message', defaultMessage, index); + } else if ( + (!message || message === resolvedActionGroupMessage) && + defaultMessage && + defaultMessage.length > 0 + ) { editAction('message', defaultMessage, index); } + // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [defaultMessage]); return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.tsx index 80a2f9d7709cc..d1498567218d3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.tsx @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { ActionParamsProps } from '../../../../types'; import { SlackActionParams } from '../types'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; +import { resolvedActionGroupMessage } from '../../../constants'; const SlackParamsFields: React.FunctionComponent> = ({ actionParams, @@ -19,11 +20,18 @@ const SlackParamsFields: React.FunctionComponent { const { message } = actionParams; useEffect(() => { - if (!message && defaultMessage && defaultMessage.length > 0) { + if (defaultMessage === resolvedActionGroupMessage) { + editAction('message', defaultMessage, index); + } else if ( + (!message || message === resolvedActionGroupMessage) && + defaultMessage && + defaultMessage.length > 0 + ) { editAction('message', defaultMessage, index); } + // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [defaultMessage]); return ( void; setHasActionsWithBrokenConnector?: (value: boolean) => void; 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 38468283b9c19..68f54e1ea351c 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, Suspense, useState } from 'react'; +import React, { Fragment, Suspense, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -25,10 +25,20 @@ import { EuiLoadingSpinner, EuiBadge, } from '@elastic/eui'; -import { IErrorObject, AlertAction, ActionTypeIndex, ActionConnector } from '../../../types'; +import { ResolvedActionGroup } from '../../../../../alerts/common'; +import { + IErrorObject, + AlertAction, + ActionTypeIndex, + ActionConnector, + ActionVariables, + ActionVariable, +} from '../../../types'; import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled'; import { hasSaveActionsCapability } from '../../lib/capabilities'; import { ActionAccordionFormProps } from './action_form'; +import { actionVariablesFromAlertType } from '../../lib/action_variables'; +import { resolvedActionGroupMessage } from '../../constants'; export type ActionTypeFormProps = { actionItem: AlertAction; @@ -88,6 +98,21 @@ export const ActionTypeForm = ({ setActionGroupIdByIndex, }: ActionTypeFormProps) => { const [isOpen, setIsOpen] = useState(true); + const [availableActionVariables, setAvailableActionVariables] = useState([]); + const [availableDefaultActionMessage, setAvailableDefaultActionMessage] = useState< + string | undefined + >(undefined); + + useEffect(() => { + setAvailableActionVariables(getAvailableActionVariables(messageVariables, actionItem.group)); + const res = + actionItem.group === ResolvedActionGroup.id + ? resolvedActionGroupMessage + : defaultActionMessage; + setAvailableDefaultActionMessage(res); + // setActionParamsProperty('message', res, index); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionItem.group]); const canSave = hasSaveActionsCapability(capabilities); const getSelectedOptions = (actionItemId: string) => { @@ -244,8 +269,8 @@ export const ActionTypeForm = ({ index={index} errors={actionParamsErrors.errors} editAction={setActionParamsProperty} - messageVariables={messageVariables} - defaultMessage={defaultActionMessage ?? undefined} + messageVariables={availableActionVariables} + defaultMessage={availableDefaultActionMessage} docLinks={docLinks} http={http} toastNotifications={toastNotifications} @@ -337,3 +362,20 @@ export const ActionTypeForm = ({ ); }; + +function getAvailableActionVariables( + actionVariables: ActionVariables | undefined, + actionGroup: string +) { + if (!actionVariables) { + return []; + } + const filteredActionVariables = + actionGroup === ResolvedActionGroup.id + ? { params: actionVariables.params, state: actionVariables.state } + : actionVariables; + + return actionVariablesFromAlertType(filteredActionVariables).sort((a, b) => + a.name.toUpperCase().localeCompare(b.name.toUpperCase()) + ); +} 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 213d1d7ad36df..c571520988509 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 @@ -40,7 +40,6 @@ import { getDurationUnitValue, } from '../../../../../alerts/common/parse_duration'; import { loadAlertTypes } from '../../lib/alert_api'; -import { actionVariablesFromAlertType } from '../../lib/action_variables'; import { AlertReducerAction } from './alert_reducer'; import { AlertTypeModel, @@ -458,9 +457,7 @@ export const AlertForm = ({ actions={alert.actions} setHasActionsDisabled={setHasActionsDisabled} setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector} - messageVariables={actionVariablesFromAlertType( - alertTypesIndex.get(alert.alertTypeId)! - ).sort((a, b) => a.name.toUpperCase().localeCompare(b.name.toUpperCase()))} + messageVariables={alertTypesIndex.get(alert.alertTypeId)!.actionVariables} defaultActionGroupId={defaultActionGroupId} actionGroups={alertTypesIndex.get(alert.alertTypeId)!.actionGroups} setActionIdByIndex={(id: string, index: number) => setActionProperty('id', id, index)} diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 1a6b68080c9a4..16c6bbc215ddc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -133,7 +133,7 @@ export interface ActionVariable { } export interface ActionVariables { - context: ActionVariable[]; + context?: ActionVariable[]; state: ActionVariable[]; params: ActionVariable[]; } From c7e6efc774dc104626f7b72adfdb6b3483c89494 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Tue, 10 Nov 2020 17:29:35 -0800 Subject: [PATCH 2/3] Added unit test --- x-pack/plugins/alerts/common/index.ts | 1 + .../application/lib/action_variables.test.ts | 8 +-- .../action_form.test.tsx | 60 +++++++++++++++++-- 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/alerts/common/index.ts b/x-pack/plugins/alerts/common/index.ts index 65aeec840da7e..0c3dcace772e4 100644 --- a/x-pack/plugins/alerts/common/index.ts +++ b/x-pack/plugins/alerts/common/index.ts @@ -12,6 +12,7 @@ export * from './alert_instance'; export * from './alert_task_instance'; export * from './alert_navigation'; export * from './alert_instance_summary'; +export * from './builtin_action_groups'; export interface ActionGroup { id: string; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts index c5009fad32942..4ea52564a3a16 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts @@ -13,7 +13,7 @@ beforeEach(() => jest.resetAllMocks()); describe('actionVariablesFromAlertType', () => { test('should return correct variables when no state or context provided', async () => { const alertType = getAlertType({ context: [], state: [], params: [] }); - expect(actionVariablesFromAlertType(alertType)).toMatchInlineSnapshot(` + expect(actionVariablesFromAlertType(alertType.actionVariables)).toMatchInlineSnapshot(` Array [ Object { "description": "The id of the alert.", @@ -48,7 +48,7 @@ describe('actionVariablesFromAlertType', () => { state: [], params: [], }); - expect(actionVariablesFromAlertType(alertType)).toMatchInlineSnapshot(` + expect(actionVariablesFromAlertType(alertType.actionVariables)).toMatchInlineSnapshot(` Array [ Object { "description": "The id of the alert.", @@ -91,7 +91,7 @@ describe('actionVariablesFromAlertType', () => { ], params: [], }); - expect(actionVariablesFromAlertType(alertType)).toMatchInlineSnapshot(` + expect(actionVariablesFromAlertType(alertType.actionVariables)).toMatchInlineSnapshot(` Array [ Object { "description": "The id of the alert.", @@ -137,7 +137,7 @@ describe('actionVariablesFromAlertType', () => { ], params: [{ name: 'fooP', description: 'fooP-description' }], }); - expect(actionVariablesFromAlertType(alertType)).toMatchInlineSnapshot(` + expect(actionVariablesFromAlertType(alertType.actionVariables)).toMatchInlineSnapshot(` Array [ Object { "description": "The id of the alert.", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 94452e70e6bfa..6593e5e264fb4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -10,6 +10,7 @@ import { act } from 'react-dom/test-utils'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult, Alert, AlertAction } from '../../../types'; import ActionForm from './action_form'; +import { ResolvedActionGroup } from '../../../../../alerts/common'; jest.mock('../../lib/action_connector_api', () => ({ loadAllActions: jest.fn(), loadActionTypes: jest.fn(), @@ -217,15 +218,22 @@ describe('action_form', () => { const wrapper = mountWithIntl( { initialAlert.actions[index].id = id; }} - actionGroups={[{ id: 'default', name: 'Default' }]} + actionGroups={[ + { id: 'default', name: 'Default' }, + { id: 'resolved', name: 'Resolved' }, + ]} setActionGroupIdByIndex={(group: string, index: number) => { initialAlert.actions[index].group = group; }} @@ -346,10 +354,52 @@ describe('action_form', () => { "inputDisplay": "Default", "value": "default", }, + Object { + "data-test-subj": "addNewActionConnectorActionGroup-0-option-resolved", + "inputDisplay": "Resolved", + "value": "resolved", + }, ] `); }); + it('renders selected Resolved action group', async () => { + const wrapper = await setup([ + { + group: ResolvedActionGroup.id, + id: 'test', + actionTypeId: actionType.id, + params: { + message: '', + }, + }, + ]); + const actionOption = wrapper.find( + `[data-test-subj="${actionType.id}-ActionTypeSelectOption"]` + ); + actionOption.first().simulate('click'); + const actionGroupsSelect = wrapper.find( + `[data-test-subj="addNewActionConnectorActionGroup-0"]` + ); + expect((actionGroupsSelect.first().props() as any).options).toMatchInlineSnapshot(` + Array [ + Object { + "data-test-subj": "addNewActionConnectorActionGroup-0-option-default", + "inputDisplay": "Default", + "value": "default", + }, + Object { + "data-test-subj": "addNewActionConnectorActionGroup-0-option-resolved", + "inputDisplay": "Resolved", + "value": "resolved", + }, + ] + `); + expect(actionGroupsSelect.first().text()).toEqual( + 'Select an option: Resolved, is selectedResolved' + ); + }); + it('renders available connectors for the selected action type', async () => { const wrapper = await setup(); const actionOption = wrapper.find( From 5dae98a7c8704d404ba019467ed6d4424eb716ab Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Wed, 11 Nov 2020 21:58:34 -0800 Subject: [PATCH 3/3] Fixed due to comments --- .../public/application/lib/action_variables.test.ts | 12 ++++++------ .../public/application/lib/action_variables.ts | 2 +- .../action_connector_form/action_type_form.tsx | 5 ++--- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts index 4ea52564a3a16..6106ba60d994b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts @@ -5,15 +5,15 @@ */ import { AlertType, ActionVariables } from '../../types'; -import { actionVariablesFromAlertType } from './action_variables'; +import { transformActionVariables } from './action_variables'; import { ALERTS_FEATURE_ID } from '../../../../alerts/common'; beforeEach(() => jest.resetAllMocks()); -describe('actionVariablesFromAlertType', () => { +describe('transformActionVariables', () => { test('should return correct variables when no state or context provided', async () => { const alertType = getAlertType({ context: [], state: [], params: [] }); - expect(actionVariablesFromAlertType(alertType.actionVariables)).toMatchInlineSnapshot(` + expect(transformActionVariables(alertType.actionVariables)).toMatchInlineSnapshot(` Array [ Object { "description": "The id of the alert.", @@ -48,7 +48,7 @@ describe('actionVariablesFromAlertType', () => { state: [], params: [], }); - expect(actionVariablesFromAlertType(alertType.actionVariables)).toMatchInlineSnapshot(` + expect(transformActionVariables(alertType.actionVariables)).toMatchInlineSnapshot(` Array [ Object { "description": "The id of the alert.", @@ -91,7 +91,7 @@ describe('actionVariablesFromAlertType', () => { ], params: [], }); - expect(actionVariablesFromAlertType(alertType.actionVariables)).toMatchInlineSnapshot(` + expect(transformActionVariables(alertType.actionVariables)).toMatchInlineSnapshot(` Array [ Object { "description": "The id of the alert.", @@ -137,7 +137,7 @@ describe('actionVariablesFromAlertType', () => { ], params: [{ name: 'fooP', description: 'fooP-description' }], }); - expect(actionVariablesFromAlertType(alertType.actionVariables)).toMatchInlineSnapshot(` + expect(transformActionVariables(alertType.actionVariables)).toMatchInlineSnapshot(` Array [ Object { "description": "The id of the alert.", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts index b8cf5b2f66cc3..2bdec1bea0c1d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { ActionVariable, ActionVariables } from '../../types'; // return a "flattened" list of action variables for an alertType -export function actionVariablesFromAlertType(actionVariables: ActionVariables): ActionVariable[] { +export function transformActionVariables(actionVariables: ActionVariables): ActionVariable[] { const alwaysProvidedVars = getAlwaysProvidedActionVariables(); const contextVars = actionVariables.context ? prefixKeys(actionVariables.context, 'context.') 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 68f54e1ea351c..bd40d35b15b2d 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 @@ -37,7 +37,7 @@ import { import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled'; import { hasSaveActionsCapability } from '../../lib/capabilities'; import { ActionAccordionFormProps } from './action_form'; -import { actionVariablesFromAlertType } from '../../lib/action_variables'; +import { transformActionVariables } from '../../lib/action_variables'; import { resolvedActionGroupMessage } from '../../constants'; export type ActionTypeFormProps = { @@ -110,7 +110,6 @@ export const ActionTypeForm = ({ ? resolvedActionGroupMessage : defaultActionMessage; setAvailableDefaultActionMessage(res); - // setActionParamsProperty('message', res, index); // eslint-disable-next-line react-hooks/exhaustive-deps }, [actionItem.group]); @@ -375,7 +374,7 @@ function getAvailableActionVariables( ? { params: actionVariables.params, state: actionVariables.state } : actionVariables; - return actionVariablesFromAlertType(filteredActionVariables).sort((a, b) => + return transformActionVariables(filteredActionVariables).sort((a, b) => a.name.toUpperCase().localeCompare(b.name.toUpperCase()) ); }