From 228387cb6e3cdc35123f5a8bc40d3ad87d4f9e52 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Sun, 15 Nov 2020 09:49:17 -0800 Subject: [PATCH] [Alerting UI] Added ability to assign alert actions to resolved action group in UI (#83139) * Added ability to assign alert actions to resolved action group in UI * Added unit test * Fixed due to comments --- .../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.test.ts | 12 ++-- .../application/lib/action_variables.ts | 12 ++-- .../action_form.test.tsx | 60 +++++++++++++++++-- .../action_connector_form/action_form.tsx | 4 +- .../action_type_form.tsx | 49 +++++++++++++-- .../sections/alert_form/alert_form.tsx | 5 +- .../triggers_actions_ui/public/types.ts | 2 +- 11 files changed, 160 insertions(+), 33 deletions(-) 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 ( 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)).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)).toMatchInlineSnapshot(` + expect(transformActionVariables(alertType.actionVariables)).toMatchInlineSnapshot(` Array [ Object { "description": "The id of the alert.", @@ -91,7 +91,7 @@ describe('actionVariablesFromAlertType', () => { ], params: [], }); - expect(actionVariablesFromAlertType(alertType)).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)).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 8bbe34847016d..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 @@ -5,14 +5,16 @@ */ import { i18n } from '@kbn/i18n'; -import { AlertType, ActionVariable } from '../../types'; +import { ActionVariable, ActionVariables } from '../../types'; // return a "flattened" list of action variables for an alertType -export function actionVariablesFromAlertType(alertType: AlertType): ActionVariable[] { +export function transformActionVariables(actionVariables: ActionVariables): ActionVariable[] { const alwaysProvidedVars = getAlwaysProvidedActionVariables(); - const contextVars = prefixKeys(alertType.actionVariables.context, 'context.'); - const paramsVars = prefixKeys(alertType.actionVariables.params, 'params.'); - const stateVars = prefixKeys(alertType.actionVariables.state, 'state.'); + const contextVars = actionVariables.context + ? prefixKeys(actionVariables.context, 'context.') + : []; + const paramsVars = prefixKeys(actionVariables.params, 'params.'); + const stateVars = prefixKeys(actionVariables.state, 'state.'); return alwaysProvidedVars.concat(contextVars, paramsVars, stateVars); } 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 c760d9128ccbd..38c9687ae581e 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( 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 3a7341afe3e07..50f5167b9e5c2 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 @@ -27,7 +27,7 @@ import { ActionTypeIndex, ActionConnector, ActionType, - ActionVariable, + ActionVariables, } from '../../../types'; import { SectionLoading } from '../../components/section_loading'; import { ConnectorAddModal } from './connector_add_modal'; @@ -51,7 +51,7 @@ export interface ActionAccordionFormProps { toastNotifications: ToastsSetup; docLinks: DocLinksStart; actionTypes?: ActionType[]; - messageVariables?: ActionVariable[]; + messageVariables?: ActionVariables; defaultActionMessage?: string; setHasActionsDisabled?: (value: boolean) => 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..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 @@ -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 { transformActionVariables } from '../../lib/action_variables'; +import { resolvedActionGroupMessage } from '../../constants'; export type ActionTypeFormProps = { actionItem: AlertAction; @@ -88,6 +98,20 @@ 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); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionItem.group]); const canSave = hasSaveActionsCapability(capabilities); const getSelectedOptions = (actionItemId: string) => { @@ -244,8 +268,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 +361,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 transformActionVariables(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[]; }