diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx b/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx index 9c3d1c90e67d7..337ca2e3c918e 100644 --- a/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx +++ b/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx @@ -109,3 +109,6 @@ export const JiraConnectorFlyout = withConnectorFlyout({ configKeys: ['projectKey'], connectorActionTypeId: '.jira', }); + +// eslint-disable-next-line import/no-default-export +export { JiraConnectorFlyout as default }; diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx b/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx index ada9608e37c98..049ccb7cf17b7 100644 --- a/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx +++ b/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { lazy } from 'react'; import { ValidationResult, // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -13,7 +14,6 @@ import { connector } from './config'; import { createActionType } from '../utils'; import logo from './logo.svg'; import { JiraActionConnector } from './types'; -import { JiraConnectorFlyout } from './flyout'; import * as i18n from './translations'; interface Errors { @@ -50,5 +50,5 @@ export const getActionType = createActionType({ selectMessage: i18n.JIRA_DESC, actionTypeTitle: connector.name, validateConnector, - actionConnectorFields: JiraConnectorFlyout, + actionConnectorFields: lazy(() => import('./flyout')), }); diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx b/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx index 5d5d08dacf90c..2783e988a6405 100644 --- a/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx @@ -82,3 +82,6 @@ export const ServiceNowConnectorFlyout = withConnectorFlyout import('./flyout')), }); diff --git a/x-pack/plugins/siem/public/lib/connectors/types.ts b/x-pack/plugins/siem/public/lib/connectors/types.ts index ffb013c347e59..3d3692c9806e4 100644 --- a/x-pack/plugins/siem/public/lib/connectors/types.ts +++ b/x-pack/plugins/siem/public/lib/connectors/types.ts @@ -8,6 +8,7 @@ /* eslint-disable @kbn/eslint/no-restricted-paths */ import { ActionType } from '../../../../triggers_actions_ui/public'; +import { IErrorObject } from '../../../../triggers_actions_ui/public/types'; import { ExternalIncidentServiceConfiguration } from '../../../../actions/server/builtin_action_types/case/types'; import { ActionType as ThirdPartySupportedActions, CaseField } from '../../../../case/common/api'; @@ -42,7 +43,7 @@ export interface ActionConnectorValidationErrors { export type Optional = Omit & Partial; export interface ConnectorFlyoutFormProps { - errors: { [key: string]: string[] }; + errors: IErrorObject; action: T; onChangeSecret: (key: string, value: string) => void; onBlurSecret: (key: string) => void; diff --git a/x-pack/plugins/siem/public/lib/connectors/utils.ts b/x-pack/plugins/siem/public/lib/connectors/utils.ts index 169b4758876e8..cc1608a05e2ce 100644 --- a/x-pack/plugins/siem/public/lib/connectors/utils.ts +++ b/x-pack/plugins/siem/public/lib/connectors/utils.ts @@ -7,7 +7,6 @@ import { ActionTypeModel, ValidationResult, - ActionParamsProps, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../triggers_actions_ui/public/types'; @@ -31,7 +30,7 @@ export const createActionType = ({ validateConnector, validateParams = connectorParamsValidator, actionConnectorFields, - actionParamsFields = ConnectorParamsFields, + actionParamsFields = null, }: Optional) => (): ActionTypeModel => { return { id, @@ -59,15 +58,6 @@ export const createActionType = ({ }; }; -const ConnectorParamsFields: React.FunctionComponent> = ({ - actionParams, - editAction, - index, - errors, -}) => { - return null; -}; - const connectorParamsValidator = (actionParams: ActionConnectorParams): ValidationResult => { return { errors: {} }; }; diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index ece1791c66e11..c5f02863ba8a1 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -985,8 +985,8 @@ Each action type should be defined as an `ActionTypeModel` object with the follo |selectMessage|Short description of action type responsibility, that will be displayed on the select card in UI.| |validateConnector|Validation function for action connector.| |validateParams|Validation function for action params.| -|actionConnectorFields|React functional component for building UI of current action type connector.| -|actionParamsFields|React functional component for building UI of current action type params. Displayed as a part of Create Alert flyout.| +|actionConnectorFields|A lazy loaded React component for building UI of current action type connector.| +|actionParamsFields|A lazy loaded React component for building UI of current action type params. Displayed as a part of Create Alert flyout.| ## Register action type model @@ -1082,8 +1082,8 @@ export function getActionType(): ActionTypeModel { } return validationResult; }, - actionConnectorFields: ExampleConnectorFields, - actionParamsFields: ExampleParamsFields, + actionConnectorFields: lazy(() => import('./example_connector_fields')), + actionParamsFields: lazy(() => import('./example_params_fields')), }; } ``` @@ -1130,6 +1130,9 @@ const ExampleConnectorFields: React.FunctionComponent ); }; + +// Export as default in order to support lazy loading +export {ExampleConnectorFields as default}; ``` 3. Define action type params fields using the property of `ActionTypeModel` `actionParamsFields`: @@ -1175,6 +1178,9 @@ const ExampleParamsFields: React.FunctionComponent ); }; + +// Export as default in order to support lazy loading +export {ExampleParamsFields as default}; ``` 4. Extend registration code with the new action type register in the file `x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts` diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index 0593940a0d105..63860e062c8da 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -3,8 +3,8 @@ * 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 from 'react'; -import { Switch, Route, Redirect, HashRouter } from 'react-router-dom'; +import React, { lazy, Suspense } from 'react'; +import { Switch, Route, Redirect, HashRouter, RouteComponentProps } from 'react-router-dom'; import { ChromeStart, DocLinksStart, @@ -15,17 +15,21 @@ import { ChromeBreadcrumb, CoreStart, } from 'kibana/public'; +import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { BASE_PATH, Section, routeToAlertDetails } from './constants'; -import { TriggersActionsUIHome } from './home'; import { AppContextProvider, useAppDependencies } from './app_context'; import { hasShowAlertsCapability } from './lib/capabilities'; import { ActionTypeModel, AlertTypeModel } from '../types'; import { TypeRegistry } from './type_registry'; -import { AlertDetailsRouteWithApi as AlertDetailsRoute } from './sections/alert_details/components/alert_details_route'; import { ChartsPluginStart } from '../../../../../src/plugins/charts/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { PluginStartContract as AlertingStart } from '../../../alerting/public'; +const TriggersActionsUIHome = lazy(async () => import('./home')); +const AlertDetailsRoute = lazy(() => + import('./sections/alert_details/components/alert_details_route') +); + export interface AppDeps { dataPlugin: DataPublicPluginStart; charts: ChartsPluginStart; @@ -62,9 +66,32 @@ export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) = const DEFAULT_SECTION: Section = canShowAlerts ? 'alerts' : 'connectors'; return ( - - {canShowAlerts && } + + {canShowAlerts && ( + + )} ); }; + +function suspendedRouteComponent( + RouteComponent: React.ComponentType> +) { + return (props: RouteComponentProps) => ( + + + + + + } + > + + + ); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.tsx deleted file mode 100644 index dff697297f3e4..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.tsx +++ /dev/null @@ -1,609 +0,0 @@ -/* - * 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, useState, useEffect } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiFieldText, - EuiFlexItem, - EuiFlexGroup, - EuiFieldNumber, - EuiFieldPassword, - EuiComboBox, - EuiTextArea, - EuiButtonEmpty, - EuiSwitch, - EuiFormRow, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { - ActionTypeModel, - ActionConnectorFieldsProps, - ValidationResult, - ActionParamsProps, -} from '../../../types'; -import { EmailActionParams, EmailActionConnector } from './types'; -import { AddMessageVariables } from '../add_message_variables'; - -export function getActionType(): ActionTypeModel { - const mailformat = /^[^@\s]+@[^@\s]+$/; - return { - id: '.email', - iconClass: 'email', - selectMessage: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText', - { - defaultMessage: 'Send email from your server.', - } - ), - actionTypeTitle: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.actionTypeTitle', - { - defaultMessage: 'Send to email', - } - ), - validateConnector: (action: EmailActionConnector): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - from: new Array(), - port: new Array(), - host: new Array(), - user: new Array(), - password: new Array(), - }; - validationResult.errors = errors; - if (!action.config.from) { - errors.from.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText', - { - defaultMessage: 'Sender is required.', - } - ) - ); - } - if (action.config.from && !action.config.from.trim().match(mailformat)) { - errors.from.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText', - { - defaultMessage: 'Sender is not a valid email address.', - } - ) - ); - } - if (!action.config.port) { - errors.port.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText', - { - defaultMessage: 'Port is required.', - } - ) - ); - } - if (!action.config.host) { - errors.host.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredHostText', - { - defaultMessage: 'Host is required.', - } - ) - ); - } - if (action.secrets.user && !action.secrets.password) { - errors.password.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPasswordText', - { - defaultMessage: 'Password is required when username is used.', - } - ) - ); - } - if (!action.secrets.user && action.secrets.password) { - errors.user.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredUserText', - { - defaultMessage: 'Username is required when password is used.', - } - ) - ); - } - return validationResult; - }, - validateParams: (actionParams: EmailActionParams): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - to: new Array(), - cc: new Array(), - bcc: new Array(), - message: new Array(), - subject: new Array(), - }; - validationResult.errors = errors; - if ( - (!(actionParams.to instanceof Array) || actionParams.to.length === 0) && - (!(actionParams.cc instanceof Array) || actionParams.cc.length === 0) && - (!(actionParams.bcc instanceof Array) || actionParams.bcc.length === 0) - ) { - const errorText = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText', - { - defaultMessage: 'No To, Cc, or Bcc entry. At least one entry is required.', - } - ); - errors.to.push(errorText); - errors.cc.push(errorText); - errors.bcc.push(errorText); - } - if (!actionParams.message?.length) { - errors.message.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredMessageText', - { - defaultMessage: 'Message is required.', - } - ) - ); - } - if (!actionParams.subject?.length) { - errors.subject.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSubjectText', - { - defaultMessage: 'Subject is required.', - } - ) - ); - } - return validationResult; - }, - actionConnectorFields: EmailActionConnectorFields, - actionParamsFields: EmailParamsFields, - }; -} - -const EmailActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors }) => { - const { from, host, port, secure } = action.config; - const { user, password } = action.secrets; - - return ( - - - - 0 && from !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.fromTextFieldLabel', - { - defaultMessage: 'Sender', - } - )} - > - 0 && from !== undefined} - name="from" - value={from || ''} - data-test-subj="emailFromInput" - onChange={e => { - editActionConfig('from', e.target.value); - }} - onBlur={() => { - if (!from) { - editActionConfig('from', ''); - } - }} - /> - - - - - - 0 && host !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hostTextFieldLabel', - { - defaultMessage: 'Host', - } - )} - > - 0 && host !== undefined} - name="host" - value={host || ''} - data-test-subj="emailHostInput" - onChange={e => { - editActionConfig('host', e.target.value); - }} - onBlur={() => { - if (!host) { - editActionConfig('host', ''); - } - }} - /> - - - - - - 0 && port !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.portTextFieldLabel', - { - defaultMessage: 'Port', - } - )} - > - 0 && port !== undefined} - fullWidth - name="port" - value={port || ''} - data-test-subj="emailPortInput" - onChange={e => { - editActionConfig('port', parseInt(e.target.value, 10)); - }} - onBlur={() => { - if (!port) { - editActionConfig('port', 0); - } - }} - /> - - - - - - { - editActionConfig('secure', e.target.checked); - }} - /> - - - - - - - - - 0} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel', - { - defaultMessage: 'Username', - } - )} - > - 0} - name="user" - value={user || ''} - data-test-subj="emailUserInput" - onChange={e => { - editActionSecrets('user', nullableString(e.target.value)); - }} - /> - - - - 0} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.passwordFieldLabel', - { - defaultMessage: 'Password', - } - )} - > - 0} - name="password" - value={password || ''} - data-test-subj="emailPasswordInput" - onChange={e => { - editActionSecrets('password', nullableString(e.target.value)); - }} - /> - - - - - ); -}; - -const EmailParamsFields: React.FunctionComponent> = ({ - actionParams, - editAction, - index, - errors, - messageVariables, - defaultMessage, -}) => { - const { to, cc, bcc, subject, message } = actionParams; - const toOptions = to ? to.map((label: string) => ({ label })) : []; - const ccOptions = cc ? cc.map((label: string) => ({ label })) : []; - const bccOptions = bcc ? bcc.map((label: string) => ({ label })) : []; - const [addCC, setAddCC] = useState(false); - const [addBCC, setAddBCC] = useState(false); - - useEffect(() => { - if (!message && defaultMessage && defaultMessage.length > 0) { - editAction('message', defaultMessage, index); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const onSelectMessageVariable = (paramsProperty: string, variable: string) => { - editAction( - paramsProperty, - ((actionParams as any)[paramsProperty] ?? '').concat(` {{${variable}}}`), - index - ); - }; - - return ( - - 0 && to !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientTextFieldLabel', - { - defaultMessage: 'To', - } - )} - labelAppend={ - - - {!addCC ? ( - setAddCC(true)}> - - - ) : null} - {!addBCC ? ( - setAddBCC(true)}> - - - ) : null} - - - } - > - 0 && to !== undefined} - fullWidth - data-test-subj="toEmailAddressInput" - selectedOptions={toOptions} - onCreateOption={(searchValue: string) => { - const newOptions = [...toOptions, { label: searchValue }]; - editAction( - 'to', - newOptions.map(newOption => newOption.label), - index - ); - }} - onChange={(selectedOptions: Array<{ label: string }>) => { - editAction( - 'to', - selectedOptions.map(selectedOption => selectedOption.label), - index - ); - }} - onBlur={() => { - if (!to) { - editAction('to', [], index); - } - }} - /> - - {addCC ? ( - 0 && cc !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientCopyTextFieldLabel', - { - defaultMessage: 'Cc', - } - )} - > - 0 && cc !== undefined} - fullWidth - data-test-subj="ccEmailAddressInput" - selectedOptions={ccOptions} - onCreateOption={(searchValue: string) => { - const newOptions = [...ccOptions, { label: searchValue }]; - editAction( - 'cc', - newOptions.map(newOption => newOption.label), - index - ); - }} - onChange={(selectedOptions: Array<{ label: string }>) => { - editAction( - 'cc', - selectedOptions.map(selectedOption => selectedOption.label), - index - ); - }} - onBlur={() => { - if (!cc) { - editAction('cc', [], index); - } - }} - /> - - ) : null} - {addBCC ? ( - 0 && bcc !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientBccTextFieldLabel', - { - defaultMessage: 'Bcc', - } - )} - > - 0 && bcc !== undefined} - fullWidth - data-test-subj="bccEmailAddressInput" - selectedOptions={bccOptions} - onCreateOption={(searchValue: string) => { - const newOptions = [...bccOptions, { label: searchValue }]; - editAction( - 'bcc', - newOptions.map(newOption => newOption.label), - index - ); - }} - onChange={(selectedOptions: Array<{ label: string }>) => { - editAction( - 'bcc', - selectedOptions.map(selectedOption => selectedOption.label), - index - ); - }} - onBlur={() => { - if (!bcc) { - editAction('bcc', [], index); - } - }} - /> - - ) : null} - 0 && subject !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.subjectTextFieldLabel', - { - defaultMessage: 'Subject', - } - )} - labelAppend={ - - onSelectMessageVariable('subject', variable) - } - paramsProperty="subject" - /> - } - > - 0 && subject !== undefined} - name="subject" - data-test-subj="emailSubjectInput" - value={subject || ''} - onChange={e => { - editAction('subject', e.target.value, index); - }} - onBlur={() => { - if (!subject) { - editAction('subject', '', index); - } - }} - /> - - 0 && message !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.messageTextAreaFieldLabel', - { - defaultMessage: 'Message', - } - )} - labelAppend={ - - onSelectMessageVariable('message', variable) - } - paramsProperty="message" - /> - } - > - 0 && message !== undefined} - value={message || ''} - name="message" - data-test-subj="emailMessageInput" - onChange={e => { - editAction('message', e.target.value, index); - }} - onBlur={() => { - if (!message) { - editAction('message', '', index); - } - }} - /> - - - ); -}; - -// if the string == null or is empty, return null, else return string -function nullableString(str: string | null | undefined) { - if (str == null || str.trim() === '') return null; - return str; -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx similarity index 62% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx index af9e34071fd09..e823e848f52c2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx @@ -3,12 +3,10 @@ * 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, { FunctionComponent } from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { TypeRegistry } from '../../type_registry'; -import { registerBuiltInActionTypes } from './index'; -import { ActionTypeModel, ActionParamsProps } from '../../../types'; -import { EmailActionParams, EmailActionConnector } from './types'; +import { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '../index'; +import { ActionTypeModel } from '../../../../types'; +import { EmailActionConnector } from '../types'; const ACTION_TYPE_ID = '.email'; let actionTypeModel: ActionTypeModel; @@ -206,80 +204,3 @@ describe('action params validation', () => { }); }); }); - -describe('EmailActionConnectorFields renders', () => { - test('all connector fields is rendered', () => { - expect(actionTypeModel.actionConnectorFields).not.toBeNull(); - if (!actionTypeModel.actionConnectorFields) { - return; - } - const ConnectorFields = actionTypeModel.actionConnectorFields; - const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.email', - name: 'email', - config: { - from: 'test@test.com', - }, - } as EmailActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - /> - ); - expect(wrapper.find('[data-test-subj="emailFromInput"]').length > 0).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="emailFromInput"]') - .first() - .prop('value') - ).toBe('test@test.com'); - expect(wrapper.find('[data-test-subj="emailHostInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="emailPortInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="emailUserInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="emailPasswordInput"]').length > 0).toBeTruthy(); - }); -}); - -describe('EmailParamsFields renders', () => { - test('all params fields is rendered', () => { - expect(actionTypeModel.actionParamsFields).not.toBeNull(); - if (!actionTypeModel.actionParamsFields) { - return; - } - const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent< - ActionParamsProps - >; - const actionParams = { - cc: [], - bcc: [], - to: ['test@test.com'], - subject: 'test', - message: 'test message', - }; - const wrapper = mountWithIntl( - {}} - index={0} - /> - ); - expect(wrapper.find('[data-test-subj="toEmailAddressInput"]').length > 0).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="toEmailAddressInput"]') - .first() - .prop('selectedOptions') - ).toStrictEqual([{ label: 'test@test.com' }]); - expect(wrapper.find('[data-test-subj="emailSubjectInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="emailMessageInput"]').length > 0).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx new file mode 100644 index 0000000000000..abb102c04b054 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx @@ -0,0 +1,150 @@ +/* + * 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 { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import { ActionTypeModel, ValidationResult } from '../../../../types'; +import { EmailActionParams, EmailActionConnector } from '../types'; + +export function getActionType(): ActionTypeModel { + const mailformat = /^[^@\s]+@[^@\s]+$/; + return { + id: '.email', + iconClass: 'email', + selectMessage: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText', + { + defaultMessage: 'Send email from your server.', + } + ), + actionTypeTitle: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.actionTypeTitle', + { + defaultMessage: 'Send to email', + } + ), + validateConnector: (action: EmailActionConnector): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + from: new Array(), + port: new Array(), + host: new Array(), + user: new Array(), + password: new Array(), + }; + validationResult.errors = errors; + if (!action.config.from) { + errors.from.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText', + { + defaultMessage: 'Sender is required.', + } + ) + ); + } + if (action.config.from && !action.config.from.trim().match(mailformat)) { + errors.from.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText', + { + defaultMessage: 'Sender is not a valid email address.', + } + ) + ); + } + if (!action.config.port) { + errors.port.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText', + { + defaultMessage: 'Port is required.', + } + ) + ); + } + if (!action.config.host) { + errors.host.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredHostText', + { + defaultMessage: 'Host is required.', + } + ) + ); + } + if (action.secrets.user && !action.secrets.password) { + errors.password.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPasswordText', + { + defaultMessage: 'Password is required when username is used.', + } + ) + ); + } + if (!action.secrets.user && action.secrets.password) { + errors.user.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredUserText', + { + defaultMessage: 'Username is required when password is used.', + } + ) + ); + } + return validationResult; + }, + validateParams: (actionParams: EmailActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + to: new Array(), + cc: new Array(), + bcc: new Array(), + message: new Array(), + subject: new Array(), + }; + validationResult.errors = errors; + if ( + (!(actionParams.to instanceof Array) || actionParams.to.length === 0) && + (!(actionParams.cc instanceof Array) || actionParams.cc.length === 0) && + (!(actionParams.bcc instanceof Array) || actionParams.bcc.length === 0) + ) { + const errorText = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText', + { + defaultMessage: 'No To, Cc, or Bcc entry. At least one entry is required.', + } + ); + errors.to.push(errorText); + errors.cc.push(errorText); + errors.bcc.push(errorText); + } + if (!actionParams.message?.length) { + errors.message.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredMessageText', + { + defaultMessage: 'Message is required.', + } + ) + ); + } + if (!actionParams.subject?.length) { + errors.subject.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSubjectText', + { + defaultMessage: 'Subject is required.', + } + ) + ); + } + return validationResult; + }, + actionConnectorFields: lazy(() => import('./email_connector')), + actionParamsFields: lazy(() => import('./email_params')), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx new file mode 100644 index 0000000000000..67514e815bc49 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx @@ -0,0 +1,47 @@ +/* + * 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 from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { EmailActionConnector } from '../types'; +import EmailActionConnectorFields from './email_connector'; +import { DocLinksStart } from 'kibana/public'; + +describe('EmailActionConnectorFields renders', () => { + test('all connector fields is rendered', () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@test.com', + }, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + /> + ); + expect(wrapper.find('[data-test-subj="emailFromInput"]').length > 0).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="emailFromInput"]') + .first() + .prop('value') + ).toBe('test@test.com'); + expect(wrapper.find('[data-test-subj="emailHostInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="emailPortInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="emailUserInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="emailPasswordInput"]').length > 0).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx new file mode 100644 index 0000000000000..4ef4c8a4d8617 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx @@ -0,0 +1,209 @@ +/* + * 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 } from 'react'; +import { + EuiFieldText, + EuiFlexItem, + EuiFlexGroup, + EuiFieldNumber, + EuiFieldPassword, + EuiSwitch, + EuiFormRow, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import { EmailActionConnector } from '../types'; + +export const EmailActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors }) => { + const { from, host, port, secure } = action.config; + const { user, password } = action.secrets; + + return ( + + + + 0 && from !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.fromTextFieldLabel', + { + defaultMessage: 'Sender', + } + )} + > + 0 && from !== undefined} + name="from" + value={from || ''} + data-test-subj="emailFromInput" + onChange={e => { + editActionConfig('from', e.target.value); + }} + onBlur={() => { + if (!from) { + editActionConfig('from', ''); + } + }} + /> + + + + + + 0 && host !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hostTextFieldLabel', + { + defaultMessage: 'Host', + } + )} + > + 0 && host !== undefined} + name="host" + value={host || ''} + data-test-subj="emailHostInput" + onChange={e => { + editActionConfig('host', e.target.value); + }} + onBlur={() => { + if (!host) { + editActionConfig('host', ''); + } + }} + /> + + + + + + 0 && port !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.portTextFieldLabel', + { + defaultMessage: 'Port', + } + )} + > + 0 && port !== undefined} + fullWidth + name="port" + value={port || ''} + data-test-subj="emailPortInput" + onChange={e => { + editActionConfig('port', parseInt(e.target.value, 10)); + }} + onBlur={() => { + if (!port) { + editActionConfig('port', 0); + } + }} + /> + + + + + + { + editActionConfig('secure', e.target.checked); + }} + /> + + + + + + + + + 0} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel', + { + defaultMessage: 'Username', + } + )} + > + 0} + name="user" + value={user || ''} + data-test-subj="emailUserInput" + onChange={e => { + editActionSecrets('user', nullableString(e.target.value)); + }} + /> + + + + 0} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.passwordFieldLabel', + { + defaultMessage: 'Password', + } + )} + > + 0} + name="password" + value={password || ''} + data-test-subj="emailPasswordInput" + onChange={e => { + editActionSecrets('password', nullableString(e.target.value)); + }} + /> + + + + + ); +}; + +// if the string == null or is empty, return null, else return string +function nullableString(str: string | null | undefined) { + if (str == null || str.trim() === '') return null; + return str; +} + +// eslint-disable-next-line import/no-default-export +export { EmailActionConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx new file mode 100644 index 0000000000000..a2b5ccf988afb --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx @@ -0,0 +1,37 @@ +/* + * 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 from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import EmailParamsFields from './email_params'; + +describe('EmailParamsFields renders', () => { + test('all params fields is rendered', () => { + const actionParams = { + cc: [], + bcc: [], + to: ['test@test.com'], + subject: 'test', + message: 'test message', + }; + const wrapper = mountWithIntl( + {}} + index={0} + /> + ); + expect(wrapper.find('[data-test-subj="toEmailAddressInput"]').length > 0).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="toEmailAddressInput"]') + .first() + .prop('selectedOptions') + ).toStrictEqual([{ label: 'test@test.com' }]); + expect(wrapper.find('[data-test-subj="emailSubjectInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="emailMessageInput"]').length > 0).toBeTruthy(); + }); +}); 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 new file mode 100644 index 0000000000000..13e791f1069e3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx @@ -0,0 +1,267 @@ +/* + * 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, useState, useEffect } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFieldText, EuiComboBox, EuiTextArea, EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionParamsProps } from '../../../../types'; +import { EmailActionParams } from '../types'; +import { AddMessageVariables } from '../../add_message_variables'; + +export const EmailParamsFields = ({ + actionParams, + editAction, + index, + errors, + messageVariables, + defaultMessage, +}: ActionParamsProps) => { + const { to, cc, bcc, subject, message } = actionParams; + const toOptions = to ? to.map((label: string) => ({ label })) : []; + const ccOptions = cc ? cc.map((label: string) => ({ label })) : []; + const bccOptions = bcc ? bcc.map((label: string) => ({ label })) : []; + const [addCC, setAddCC] = useState(false); + const [addBCC, setAddBCC] = useState(false); + + useEffect(() => { + if (!message && defaultMessage && defaultMessage.length > 0) { + editAction('message', defaultMessage, index); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const onSelectMessageVariable = (paramsProperty: string, variable: string) => { + editAction( + paramsProperty, + ((actionParams as any)[paramsProperty] ?? '').concat(` {{${variable}}}`), + index + ); + }; + + return ( + + 0 && to !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientTextFieldLabel', + { + defaultMessage: 'To', + } + )} + labelAppend={ + + + {!addCC ? ( + setAddCC(true)}> + + + ) : null} + {!addBCC ? ( + setAddBCC(true)}> + + + ) : null} + + + } + > + 0 && to !== undefined} + fullWidth + data-test-subj="toEmailAddressInput" + selectedOptions={toOptions} + onCreateOption={(searchValue: string) => { + const newOptions = [...toOptions, { label: searchValue }]; + editAction( + 'to', + newOptions.map(newOption => newOption.label), + index + ); + }} + onChange={(selectedOptions: Array<{ label: string }>) => { + editAction( + 'to', + selectedOptions.map(selectedOption => selectedOption.label), + index + ); + }} + onBlur={() => { + if (!to) { + editAction('to', [], index); + } + }} + /> + + {addCC ? ( + 0 && cc !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientCopyTextFieldLabel', + { + defaultMessage: 'Cc', + } + )} + > + 0 && cc !== undefined} + fullWidth + data-test-subj="ccEmailAddressInput" + selectedOptions={ccOptions} + onCreateOption={(searchValue: string) => { + const newOptions = [...ccOptions, { label: searchValue }]; + editAction( + 'cc', + newOptions.map(newOption => newOption.label), + index + ); + }} + onChange={(selectedOptions: Array<{ label: string }>) => { + editAction( + 'cc', + selectedOptions.map(selectedOption => selectedOption.label), + index + ); + }} + onBlur={() => { + if (!cc) { + editAction('cc', [], index); + } + }} + /> + + ) : null} + {addBCC ? ( + 0 && bcc !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientBccTextFieldLabel', + { + defaultMessage: 'Bcc', + } + )} + > + 0 && bcc !== undefined} + fullWidth + data-test-subj="bccEmailAddressInput" + selectedOptions={bccOptions} + onCreateOption={(searchValue: string) => { + const newOptions = [...bccOptions, { label: searchValue }]; + editAction( + 'bcc', + newOptions.map(newOption => newOption.label), + index + ); + }} + onChange={(selectedOptions: Array<{ label: string }>) => { + editAction( + 'bcc', + selectedOptions.map(selectedOption => selectedOption.label), + index + ); + }} + onBlur={() => { + if (!bcc) { + editAction('bcc', [], index); + } + }} + /> + + ) : null} + 0 && subject !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.subjectTextFieldLabel', + { + defaultMessage: 'Subject', + } + )} + labelAppend={ + + onSelectMessageVariable('subject', variable) + } + paramsProperty="subject" + /> + } + > + 0 && subject !== undefined} + name="subject" + data-test-subj="emailSubjectInput" + value={subject || ''} + onChange={e => { + editAction('subject', e.target.value, index); + }} + onBlur={() => { + if (!subject) { + editAction('subject', '', index); + } + }} + /> + + 0 && message !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.messageTextAreaFieldLabel', + { + defaultMessage: 'Message', + } + )} + labelAppend={ + + onSelectMessageVariable('message', variable) + } + paramsProperty="message" + /> + } + > + 0 && message !== undefined} + value={message || ''} + name="message" + data-test-subj="emailMessageInput" + onChange={e => { + editAction('message', e.target.value, index); + }} + onBlur={() => { + if (!message) { + editAction('message', '', index); + } + }} + /> + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { EmailParamsFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/index.ts new file mode 100644 index 0000000000000..e0dd24a44aa8f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { getActionType as getEmailActionType } from './email'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx deleted file mode 100644 index 04dc7b484ed48..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx +++ /dev/null @@ -1,240 +0,0 @@ -/* - * 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, { FunctionComponent } from 'react'; -import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { act } from 'react-dom/test-utils'; -import { TypeRegistry } from '../../type_registry'; -import { registerBuiltInActionTypes } from './index'; -import { ActionTypeModel, ActionParamsProps } from '../../../types'; -import { IndexActionParams, EsIndexActionConnector } from './types'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; -jest.mock('../../../common/index_controls', () => ({ - firstFieldOption: jest.fn(), - getFields: jest.fn(), - getIndexOptions: jest.fn(), - getIndexPatterns: jest.fn(), -})); - -const ACTION_TYPE_ID = '.index'; -let actionTypeModel: ActionTypeModel; -let deps: any; - -beforeAll(async () => { - const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); - const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); - if (getResult !== null) { - actionTypeModel = getResult; - } - const mocks = coreMock.createSetup(); - const [ - { - application: { capabilities }, - }, - ] = await mocks.getStartServices(); - deps = { - toastNotifications: mocks.notifications.toasts, - http: mocks.http, - capabilities: { - ...capabilities, - actions: { - delete: true, - save: true, - show: true, - }, - }, - actionTypeRegistry: actionTypeRegistry as any, - docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, - }; -}); - -describe('actionTypeRegistry.get() works', () => { - test('action type .index is registered', () => { - expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); - expect(actionTypeModel.iconClass).toEqual('indexOpen'); - }); -}); - -describe('index connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.index', - name: 'es_index', - config: { - index: 'test_es_index', - refresh: false, - executionTimeField: '1', - }, - } as EsIndexActionConnector; - - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - index: [], - }, - }); - }); -}); - -describe('index connector validation with minimal config', () => { - test('connector validation succeeds when connector config is valid', () => { - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.index', - name: 'es_index', - config: { - index: 'test_es_index', - }, - } as EsIndexActionConnector; - - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - index: [], - }, - }); - }); -}); - -describe('action params validation', () => { - test('action params validation succeeds when action params is valid', () => { - const actionParams = { - documents: ['test'], - }; - - expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: {}, - }); - - const emptyActionParams = {}; - - expect(actionTypeModel.validateParams(emptyActionParams)).toEqual({ - errors: {}, - }); - }); -}); - -describe('IndexActionConnectorFields renders', () => { - test('all connector fields is rendered', async () => { - expect(actionTypeModel.actionConnectorFields).not.toBeNull(); - if (!actionTypeModel.actionConnectorFields) { - return; - } - - const { getIndexPatterns } = jest.requireMock('../../../common/index_controls'); - getIndexPatterns.mockResolvedValueOnce([ - { - id: 'indexPattern1', - attributes: { - title: 'indexPattern1', - }, - }, - { - id: 'indexPattern2', - attributes: { - title: 'indexPattern2', - }, - }, - ]); - const { getFields } = jest.requireMock('../../../common/index_controls'); - getFields.mockResolvedValueOnce([ - { - type: 'date', - name: 'test1', - }, - { - type: 'text', - name: 'test2', - }, - ]); - const ConnectorFields = actionTypeModel.actionConnectorFields; - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.index', - name: 'es_index', - config: { - index: 'test', - refresh: false, - executionTimeField: 'test1', - }, - } as EsIndexActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - http={deps!.http} - /> - ); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').length > 0).toBeTruthy(); - - const indexSearchBoxValue = wrapper.find('[data-test-subj="comboBoxSearchInput"]'); - expect(indexSearchBoxValue.first().props().value).toEqual(''); - - const indexComboBox = wrapper.find('#indexConnectorSelectSearchBox'); - indexComboBox.first().simulate('click'); - const event = { target: { value: 'indexPattern1' } }; - indexComboBox - .find('input') - .first() - .simulate('change', event); - - const indexSearchBoxValueBeforeEnterData = wrapper.find( - '[data-test-subj="comboBoxSearchInput"]' - ); - expect(indexSearchBoxValueBeforeEnterData.first().props().value).toEqual('indexPattern1'); - - const indexComboBoxClear = wrapper.find('[data-test-subj="comboBoxClearButton"]'); - indexComboBoxClear.first().simulate('click'); - - const indexSearchBoxValueAfterEnterData = wrapper.find( - '[data-test-subj="comboBoxSearchInput"]' - ); - expect(indexSearchBoxValueAfterEnterData.first().props().value).toEqual('indexPattern1'); - }); -}); - -describe('IndexParamsFields renders', () => { - test('all params fields is rendered', () => { - expect(actionTypeModel.actionParamsFields).not.toBeNull(); - if (!actionTypeModel.actionParamsFields) { - return; - } - const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent< - ActionParamsProps - >; - const actionParams = { - documents: [{ test: 123 }], - }; - const wrapper = mountWithIntl( - {}} - index={0} - /> - ); - expect( - wrapper - .find('[data-test-subj="actionIndexDoc"]') - .first() - .prop('value') - ).toBe(`{ - "test": 123 -}`); - expect(wrapper.find('[data-test-subj="documentsAddVariableButton"]').length > 0).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx new file mode 100644 index 0000000000000..417a9e09086a2 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx @@ -0,0 +1,88 @@ +/* + * 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 { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '../index'; +import { ActionTypeModel } from '../../../../types'; +import { EsIndexActionConnector } from '../types'; + +const ACTION_TYPE_ID = '.index'; +let actionTypeModel: ActionTypeModel; + +beforeAll(() => { + const actionTypeRegistry = new TypeRegistry(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type .index is registered', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + expect(actionTypeModel.iconClass).toEqual('indexOpen'); + }); +}); + +describe('index connector validation', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.index', + name: 'es_index', + config: { + index: 'test_es_index', + refresh: false, + executionTimeField: '1', + }, + } as EsIndexActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + index: [], + }, + }); + }); +}); + +describe('index connector validation with minimal config', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.index', + name: 'es_index', + config: { + index: 'test_es_index', + }, + } as EsIndexActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + index: [], + }, + }); + }); +}); + +describe('action params validation', () => { + test('action params validation succeeds when action params is valid', () => { + const actionParams = { + documents: ['test'], + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: {}, + }); + + const emptyActionParams = {}; + + expect(actionTypeModel.validateParams(emptyActionParams)).toEqual({ + errors: {}, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx new file mode 100644 index 0000000000000..3ee663a5fc8a0 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx @@ -0,0 +1,51 @@ +/* + * 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 { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import { ActionTypeModel, ValidationResult } from '../../../../types'; +import { EsIndexActionConnector, IndexActionParams } from '../types'; + +export function getActionType(): ActionTypeModel { + return { + id: '.index', + iconClass: 'indexOpen', + selectMessage: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.selectMessageText', + { + defaultMessage: 'Index data into Elasticsearch.', + } + ), + actionTypeTitle: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.actionTypeTitle', + { + defaultMessage: 'Index data', + } + ), + validateConnector: (action: EsIndexActionConnector): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + index: new Array(), + }; + validationResult.errors = errors; + if (!action.config.index) { + errors.index.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.requiredIndexText', + { + defaultMessage: 'Index is required.', + } + ) + ); + } + return validationResult; + }, + actionConnectorFields: lazy(() => import('./es_index_connector')), + actionParamsFields: lazy(() => import('./es_index_params')), + validateParams: (): ValidationResult => { + return { errors: {} }; + }, + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx new file mode 100644 index 0000000000000..b0f21afeaa96c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx @@ -0,0 +1,126 @@ +/* + * 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 from 'react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { act } from 'react-dom/test-utils'; +import { EsIndexActionConnector } from '../types'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import IndexActionConnectorFields from './es_index_connector'; +import { TypeRegistry } from '../../../type_registry'; +import { DocLinksStart } from 'kibana/public'; + +jest.mock('../../../../common/index_controls', () => ({ + firstFieldOption: jest.fn(), + getFields: jest.fn(), + getIndexOptions: jest.fn(), + getIndexPatterns: jest.fn(), +})); + +describe('IndexActionConnectorFields renders', () => { + test('all connector fields is rendered', async () => { + const mocks = coreMock.createSetup(); + const [ + { + application: { capabilities }, + }, + ] = await mocks.getStartServices(); + const deps = { + toastNotifications: mocks.notifications.toasts, + http: mocks.http, + capabilities: { + ...capabilities, + actions: { + delete: true, + save: true, + show: true, + }, + }, + actionTypeRegistry: {} as TypeRegistry, + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + + const { getIndexPatterns } = jest.requireMock('../../../../common/index_controls'); + getIndexPatterns.mockResolvedValueOnce([ + { + id: 'indexPattern1', + attributes: { + title: 'indexPattern1', + }, + }, + { + id: 'indexPattern2', + attributes: { + title: 'indexPattern2', + }, + }, + ]); + const { getFields } = jest.requireMock('../../../../common/index_controls'); + getFields.mockResolvedValueOnce([ + { + type: 'date', + name: 'test1', + }, + { + type: 'text', + name: 'test2', + }, + ]); + + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.index', + name: 'es_index', + config: { + index: 'test', + refresh: false, + executionTimeField: 'test1', + }, + } as EsIndexActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + http={deps!.http} + docLinks={deps!.docLinks} + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').length > 0).toBeTruthy(); + + const indexSearchBoxValue = wrapper.find('[data-test-subj="comboBoxSearchInput"]'); + expect(indexSearchBoxValue.first().props().value).toEqual(''); + + const indexComboBox = wrapper.find('#indexConnectorSelectSearchBox'); + indexComboBox.first().simulate('click'); + const event = { target: { value: 'indexPattern1' } }; + indexComboBox + .find('input') + .first() + .simulate('change', event); + + const indexSearchBoxValueBeforeEnterData = wrapper.find( + '[data-test-subj="comboBoxSearchInput"]' + ); + expect(indexSearchBoxValueBeforeEnterData.first().props().value).toEqual('indexPattern1'); + + const indexComboBoxClear = wrapper.find('[data-test-subj="comboBoxClearButton"]'); + indexComboBoxClear.first().simulate('click'); + + const indexSearchBoxValueAfterEnterData = wrapper.find( + '[data-test-subj="comboBoxSearchInput"]' + ); + expect(indexSearchBoxValueAfterEnterData.first().props().value).toEqual('indexPattern1'); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx similarity index 66% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx index 861d6ad7284c2..9cd3a18545345 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx @@ -3,12 +3,11 @@ * 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, useState, useEffect } from 'react'; +import React, { useState, useEffect } from 'react'; import { EuiFormRow, EuiSwitch, EuiSpacer, - EuiCodeEditor, EuiComboBox, EuiComboBoxOptionOption, EuiSelect, @@ -17,64 +16,15 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { useXJsonMode } from '../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; -import { - ActionTypeModel, - ActionConnectorFieldsProps, - ValidationResult, - ActionParamsProps, -} from '../../../types'; -import { IndexActionParams, EsIndexActionConnector } from './types'; -import { getTimeFieldOptions } from '../../../common/lib/get_time_options'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import { EsIndexActionConnector } from '.././types'; +import { getTimeFieldOptions } from '../../../../common/lib/get_time_options'; import { firstFieldOption, getFields, getIndexOptions, getIndexPatterns, -} from '../../../common/index_controls'; -import { AddMessageVariables } from '../add_message_variables'; - -export function getActionType(): ActionTypeModel { - return { - id: '.index', - iconClass: 'indexOpen', - selectMessage: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.selectMessageText', - { - defaultMessage: 'Index data into Elasticsearch.', - } - ), - actionTypeTitle: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.actionTypeTitle', - { - defaultMessage: 'Index data', - } - ), - validateConnector: (action: EsIndexActionConnector): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - index: new Array(), - }; - validationResult.errors = errors; - if (!action.config.index) { - errors.index.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.requiredIndexText', - { - defaultMessage: 'Index is required.', - } - ) - ); - } - return validationResult; - }, - actionConnectorFields: IndexActionConnectorFields, - actionParamsFields: IndexParamsFields, - validateParams: (): ValidationResult => { - return { errors: {} }; - }, - }; -} +} from '../../../../common/index_controls'; const IndexActionConnectorFields: React.FunctionComponent> = ({ - actionParams, - index, - editAction, - messageVariables, -}) => { - const { documents } = actionParams; - const { xJsonMode, convertToJson, setXJson, xJson } = useXJsonMode( - documents && documents.length > 0 ? documents[0] : null - ); - const onSelectMessageVariable = (variable: string) => { - const value = (xJson ?? '').concat(` {{${variable}}}`); - setXJson(value); - // Keep the documents in sync with the editor content - onDocumentsChange(convertToJson(value)); - }; - - function onDocumentsChange(updatedDocuments: string) { - try { - const documentsJSON = JSON.parse(updatedDocuments); - editAction('documents', [documentsJSON], index); - // eslint-disable-next-line no-empty - } catch (e) {} - } - return ( - - onSelectMessageVariable(variable)} - paramsProperty="documents" - /> - } - > - { - setXJson(xjson); - // Keep the documents in sync with the editor content - onDocumentsChange(convertToJson(xjson)); - }} - /> - - - ); -}; - // if the string == null or is empty, return null, else return string function nullableString(str: string | null | undefined) { if (str == null || str.trim() === '') return null; return str; } + +// eslint-disable-next-line import/no-default-export +export { IndexActionConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx new file mode 100644 index 0000000000000..5f05a56a228e2 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx @@ -0,0 +1,33 @@ +/* + * 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 from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import ParamsFields from './es_index_params'; + +describe('IndexParamsFields renders', () => { + test('all params fields is rendered', () => { + const actionParams = { + documents: [{ test: 123 }], + }; + const wrapper = mountWithIntl( + {}} + index={0} + /> + ); + expect( + wrapper + .find('[data-test-subj="actionIndexDoc"]') + .first() + .prop('value') + ).toBe(`{ + "test": 123 +}`); + expect(wrapper.find('[data-test-subj="documentsAddVariableButton"]').length > 0).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx new file mode 100644 index 0000000000000..0b095cdc26984 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx @@ -0,0 +1,81 @@ +/* + * 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 } from 'react'; +import { EuiFormRow, EuiCodeEditor } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useXJsonMode } from '../../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; +import { ActionParamsProps } from '../../../../types'; +import { IndexActionParams } from '.././types'; +import { AddMessageVariables } from '../../add_message_variables'; + +export const IndexParamsFields = ({ + actionParams, + index, + editAction, + messageVariables, +}: ActionParamsProps) => { + const { documents } = actionParams; + const { xJsonMode, convertToJson, setXJson, xJson } = useXJsonMode( + documents && documents.length > 0 ? documents[0] : null + ); + const onSelectMessageVariable = (variable: string) => { + const value = (xJson ?? '').concat(` {{${variable}}}`); + setXJson(value); + // Keep the documents in sync with the editor content + onDocumentsChange(convertToJson(value)); + }; + + function onDocumentsChange(updatedDocuments: string) { + try { + const documentsJSON = JSON.parse(updatedDocuments); + editAction('documents', [documentsJSON], index); + // eslint-disable-next-line no-empty + } catch (e) {} + } + return ( + + onSelectMessageVariable(variable)} + paramsProperty="documents" + /> + } + > + { + setXJson(xjson); + // Keep the documents in sync with the editor content + onDocumentsChange(convertToJson(xjson)); + }} + /> + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { IndexParamsFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/index.ts new file mode 100644 index 0000000000000..6a2ebd9c4bc71 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { getActionType as getIndexActionType } from './es_index'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts index 6ffd9b2c9ffde..8f49fa46dd54e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getActionType as getServerLogActionType } from './server_log'; -import { getActionType as getSlackActionType } from './slack'; -import { getActionType as getEmailActionType } from './email'; -import { getActionType as getIndexActionType } from './es_index'; -import { getActionType as getPagerDutyActionType } from './pagerduty'; -import { getActionType as getWebhookActionType } from './webhook'; +import { getServerLogActionType } from './server_log'; +import { getSlackActionType } from './slack'; +import { getEmailActionType } from './email'; +import { getIndexActionType } from './es_index'; +import { getPagerDutyActionType } from './pagerduty'; +import { getWebhookActionType } from './webhook'; import { TypeRegistry } from '../../type_registry'; import { ActionTypeModel } from '../../../types'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx deleted file mode 100644 index f628457dc5162..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx +++ /dev/null @@ -1,200 +0,0 @@ -/* - * 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, { FunctionComponent } from 'react'; -import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { act } from 'react-dom/test-utils'; -import { TypeRegistry } from '../../type_registry'; -import { registerBuiltInActionTypes } from './index'; -import { ActionTypeModel, ActionParamsProps } from '../../../types'; -import { - PagerDutyActionParams, - EventActionOptions, - SeverityActionOptions, - PagerDutyActionConnector, -} from './types'; - -const ACTION_TYPE_ID = '.pagerduty'; -let actionTypeModel: ActionTypeModel; -let deps: any; - -beforeAll(async () => { - const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); - const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); - if (getResult !== null) { - actionTypeModel = getResult; - } - deps = { - docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, - }; -}); - -describe('actionTypeRegistry.get() works', () => { - test('action type static data is as expected', () => { - expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); - expect(actionTypeModel.iconClass).toEqual('test-file-stub'); - }); -}); - -describe('pagerduty connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { - const actionConnector = { - secrets: { - routingKey: 'test', - }, - id: 'test', - actionTypeId: '.pagerduty', - name: 'pagerduty', - config: { - apiUrl: 'http:\\test', - }, - } as PagerDutyActionConnector; - - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - routingKey: [], - }, - }); - - delete actionConnector.config.apiUrl; - actionConnector.secrets.routingKey = 'test1'; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - routingKey: [], - }, - }); - }); - - test('connector validation fails when connector config is not valid', () => { - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.pagerduty', - name: 'pagerduty', - config: { - apiUrl: 'http:\\test', - }, - } as PagerDutyActionConnector; - - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - routingKey: ['A routing key is required.'], - }, - }); - }); -}); - -describe('pagerduty action params validation', () => { - test('action params validation succeeds when action params is valid', () => { - const actionParams = { - eventAction: 'trigger', - dedupKey: 'test', - summary: '2323', - source: 'source', - severity: 'critical', - timestamp: new Date().toISOString(), - component: 'test', - group: 'group', - class: 'test class', - }; - - expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { - summary: [], - timestamp: [], - }, - }); - }); -}); - -describe('PagerDutyActionConnectorFields renders', () => { - test('all connector fields is rendered', async () => { - expect(actionTypeModel.actionConnectorFields).not.toBeNull(); - if (!actionTypeModel.actionConnectorFields) { - return; - } - const ConnectorFields = actionTypeModel.actionConnectorFields; - const actionConnector = { - secrets: { - routingKey: 'test', - }, - id: 'test', - actionTypeId: '.pagerduty', - name: 'pagerduty', - config: { - apiUrl: 'http:\\test', - }, - } as PagerDutyActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - docLinks={deps!.docLinks} - /> - ); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - expect(wrapper.find('[data-test-subj="pagerdutyApiUrlInput"]').length > 0).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="pagerdutyApiUrlInput"]') - .first() - .prop('value') - ).toBe('http:\\test'); - expect(wrapper.find('[data-test-subj="pagerdutyRoutingKeyInput"]').length > 0).toBeTruthy(); - }); -}); - -describe('PagerDutyParamsFields renders', () => { - test('all params fields is rendered', () => { - expect(actionTypeModel.actionParamsFields).not.toBeNull(); - if (!actionTypeModel.actionParamsFields) { - return; - } - const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent< - ActionParamsProps - >; - const actionParams = { - eventAction: EventActionOptions.TRIGGER, - dedupKey: 'test', - summary: '2323', - source: 'source', - severity: SeverityActionOptions.CRITICAL, - timestamp: new Date().toISOString(), - component: 'test', - group: 'group', - class: 'test class', - }; - const wrapper = mountWithIntl( - {}} - index={0} - /> - ); - expect(wrapper.find('[data-test-subj="severitySelect"]').length > 0).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="severitySelect"]') - .first() - .prop('value') - ).toStrictEqual('critical'); - expect(wrapper.find('[data-test-subj="eventActionSelect"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="dedupKeyInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="timestampInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="componentInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="groupInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="sourceInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="pagerdutySummaryInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="dedupKeyAddVariableButton"]').length > 0).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/index.ts new file mode 100644 index 0000000000000..9128ec81391ab --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { getActionType as getPagerDutyActionType } from './pagerduty'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.svg b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.svg similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.svg rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.svg diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx new file mode 100644 index 0000000000000..ba7eb598c120d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx @@ -0,0 +1,99 @@ +/* + * 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 { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '.././index'; +import { ActionTypeModel } from '../../../../types'; +import { PagerDutyActionConnector } from '.././types'; + +const ACTION_TYPE_ID = '.pagerduty'; +let actionTypeModel: ActionTypeModel; + +beforeAll(() => { + const actionTypeRegistry = new TypeRegistry(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + expect(actionTypeModel.iconClass).toEqual('test-file-stub'); + }); +}); + +describe('pagerduty connector validation', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: { + routingKey: 'test', + }, + id: 'test', + actionTypeId: '.pagerduty', + name: 'pagerduty', + config: { + apiUrl: 'http:\\test', + }, + } as PagerDutyActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + routingKey: [], + }, + }); + + delete actionConnector.config.apiUrl; + actionConnector.secrets.routingKey = 'test1'; + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + routingKey: [], + }, + }); + }); + + test('connector validation fails when connector config is not valid', () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.pagerduty', + name: 'pagerduty', + config: { + apiUrl: 'http:\\test', + }, + } as PagerDutyActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + routingKey: ['A routing key is required.'], + }, + }); + }); +}); + +describe('pagerduty action params validation', () => { + test('action params validation succeeds when action params is valid', () => { + const actionParams = { + eventAction: 'trigger', + dedupKey: 'test', + summary: '2323', + source: 'source', + severity: 'critical', + timestamp: new Date().toISOString(), + component: 'test', + group: 'group', + class: 'test class', + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + summary: [], + timestamp: [], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx new file mode 100644 index 0000000000000..5e29fca397180 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx @@ -0,0 +1,96 @@ +/* + * 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 { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { ActionTypeModel, ValidationResult } from '../../../../types'; +import { PagerDutyActionParams, PagerDutyActionConnector } from '.././types'; +import pagerDutySvg from './pagerduty.svg'; +import { hasMustacheTokens } from '../../../lib/has_mustache_tokens'; + +export function getActionType(): ActionTypeModel { + return { + id: '.pagerduty', + iconClass: pagerDutySvg, + selectMessage: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.selectMessageText', + { + defaultMessage: 'Send an event in PagerDuty.', + } + ), + actionTypeTitle: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle', + { + defaultMessage: 'Send to PagerDuty', + } + ), + validateConnector: (action: PagerDutyActionConnector): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + routingKey: new Array(), + }; + validationResult.errors = errors; + if (!action.secrets.routingKey) { + errors.routingKey.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredRoutingKeyText', + { + defaultMessage: 'A routing key is required.', + } + ) + ); + } + return validationResult; + }, + validateParams: (actionParams: PagerDutyActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + summary: new Array(), + timestamp: new Array(), + }; + validationResult.errors = errors; + if (!actionParams.summary?.length) { + errors.summary.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredSummaryText', + { + defaultMessage: 'Summary is required.', + } + ) + ); + } + if (actionParams.timestamp && !hasMustacheTokens(actionParams.timestamp)) { + if (isNaN(Date.parse(actionParams.timestamp))) { + const { nowShortFormat, nowLongFormat } = getValidTimestampExamples(); + errors.timestamp.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.invalidTimestamp', + { + defaultMessage: + 'Timestamp must be a valid date, such as {nowShortFormat} or {nowLongFormat}.', + values: { + nowShortFormat, + nowLongFormat, + }, + } + ) + ); + } + } + return validationResult; + }, + actionConnectorFields: lazy(() => import('./pagerduty_connectors')), + actionParamsFields: lazy(() => import('./pagerduty_params')), + }; +} + +function getValidTimestampExamples() { + const now = moment(); + return { + nowShortFormat: now.format('YYYY-MM-DD'), + nowLongFormat: now.format('YYYY-MM-DD h:mm:ss'), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx new file mode 100644 index 0000000000000..3f3fba1599bd2 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx @@ -0,0 +1,54 @@ +/* + * 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 from 'react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { act } from 'react-dom/test-utils'; +import { PagerDutyActionConnector } from '.././types'; +import PagerDutyActionConnectorFields from './pagerduty_connectors'; +import { DocLinksStart } from 'kibana/public'; + +describe('PagerDutyActionConnectorFields renders', () => { + test('all connector fields is rendered', async () => { + const actionConnector = { + secrets: { + routingKey: 'test', + }, + id: 'test', + actionTypeId: '.pagerduty', + name: 'pagerduty', + config: { + apiUrl: 'http:\\test', + }, + } as PagerDutyActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="pagerdutyApiUrlInput"]').length > 0).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="pagerdutyApiUrlInput"]') + .first() + .prop('value') + ).toBe('http:\\test'); + expect(wrapper.find('[data-test-subj="pagerdutyRoutingKeyInput"]').length > 0).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx new file mode 100644 index 0000000000000..48da3f1778b48 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx @@ -0,0 +1,89 @@ +/* + * 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 } from 'react'; +import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import { PagerDutyActionConnector } from '.././types'; + +const PagerDutyActionConnectorFields: React.FunctionComponent> = ({ errors, action, editActionConfig, editActionSecrets, docLinks }) => { + const { apiUrl } = action.config; + const { routingKey } = action.secrets; + return ( + + + ) => { + editActionConfig('apiUrl', e.target.value); + }} + onBlur={() => { + if (!apiUrl) { + editActionConfig('apiUrl', ''); + } + }} + /> + + + + + } + error={errors.routingKey} + isInvalid={errors.routingKey.length > 0 && routingKey !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyTextFieldLabel', + { + defaultMessage: 'Integration key', + } + )} + > + 0 && routingKey !== undefined} + name="routingKey" + value={routingKey || ''} + data-test-subj="pagerdutyRoutingKeyInput" + onChange={(e: React.ChangeEvent) => { + editActionSecrets('routingKey', e.target.value); + }} + onBlur={() => { + if (!routingKey) { + editActionSecrets('routingKey', ''); + } + }} + /> + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { PagerDutyActionConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx new file mode 100644 index 0000000000000..d1b32f545c335 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx @@ -0,0 +1,48 @@ +/* + * 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 from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { EventActionOptions, SeverityActionOptions } from '.././types'; +import PagerDutyParamsFields from './pagerduty_params'; + +describe('PagerDutyParamsFields renders', () => { + test('all params fields is rendered', () => { + const actionParams = { + eventAction: EventActionOptions.TRIGGER, + dedupKey: 'test', + summary: '2323', + source: 'source', + severity: SeverityActionOptions.CRITICAL, + timestamp: new Date().toISOString(), + component: 'test', + group: 'group', + class: 'test class', + }; + const wrapper = mountWithIntl( + {}} + index={0} + /> + ); + expect(wrapper.find('[data-test-subj="severitySelect"]').length > 0).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="severitySelect"]') + .first() + .prop('value') + ).toStrictEqual('critical'); + expect(wrapper.find('[data-test-subj="eventActionSelect"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="dedupKeyInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="timestampInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="componentInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="groupInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="sourceInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="pagerdutySummaryInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="dedupKeyAddVariableButton"]').length > 0).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx similarity index 67% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx index 5ad1f2fffecce..590eba5dad936 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx @@ -4,178 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { Fragment } from 'react'; -import { - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiSelect, - EuiLink, -} from '@elastic/eui'; +import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import moment from 'moment'; -import { - ActionTypeModel, - ActionConnectorFieldsProps, - ValidationResult, - ActionParamsProps, -} from '../../../types'; -import { PagerDutyActionParams, PagerDutyActionConnector } from './types'; -import pagerDutySvg from './pagerduty.svg'; -import { AddMessageVariables } from '../add_message_variables'; -import { hasMustacheTokens } from '../../lib/has_mustache_tokens'; - -export function getActionType(): ActionTypeModel { - return { - id: '.pagerduty', - iconClass: pagerDutySvg, - selectMessage: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.selectMessageText', - { - defaultMessage: 'Send an event in PagerDuty.', - } - ), - actionTypeTitle: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle', - { - defaultMessage: 'Send to PagerDuty', - } - ), - validateConnector: (action: PagerDutyActionConnector): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - routingKey: new Array(), - }; - validationResult.errors = errors; - if (!action.secrets.routingKey) { - errors.routingKey.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredRoutingKeyText', - { - defaultMessage: 'A routing key is required.', - } - ) - ); - } - return validationResult; - }, - validateParams: (actionParams: PagerDutyActionParams): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - summary: new Array(), - timestamp: new Array(), - }; - validationResult.errors = errors; - if (!actionParams.summary?.length) { - errors.summary.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredSummaryText', - { - defaultMessage: 'Summary is required.', - } - ) - ); - } - if (actionParams.timestamp && !hasMustacheTokens(actionParams.timestamp)) { - if (isNaN(Date.parse(actionParams.timestamp))) { - const { nowShortFormat, nowLongFormat } = getValidTimestampExamples(); - errors.timestamp.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.invalidTimestamp', - { - defaultMessage: - 'Timestamp must be a valid date, such as {nowShortFormat} or {nowLongFormat}.', - values: { - nowShortFormat, - nowLongFormat, - }, - } - ) - ); - } - } - return validationResult; - }, - actionConnectorFields: PagerDutyActionConnectorFields, - actionParamsFields: PagerDutyParamsFields, - }; -} - -const PagerDutyActionConnectorFields: React.FunctionComponent> = ({ errors, action, editActionConfig, editActionSecrets, docLinks }) => { - const { apiUrl } = action.config; - const { routingKey } = action.secrets; - return ( - - - ) => { - editActionConfig('apiUrl', e.target.value); - }} - onBlur={() => { - if (!apiUrl) { - editActionConfig('apiUrl', ''); - } - }} - /> - - - - - } - error={errors.routingKey} - isInvalid={errors.routingKey.length > 0 && routingKey !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyTextFieldLabel', - { - defaultMessage: 'Integration key', - } - )} - > - 0 && routingKey !== undefined} - name="routingKey" - value={routingKey || ''} - data-test-subj="pagerdutyRoutingKeyInput" - onChange={(e: React.ChangeEvent) => { - editActionSecrets('routingKey', e.target.value); - }} - onBlur={() => { - if (!routingKey) { - editActionSecrets('routingKey', ''); - } - }} - /> - - - ); -}; +import { ActionParamsProps } from '../../../../types'; +import { PagerDutyActionParams } from '.././types'; +import { AddMessageVariables } from '../../add_message_variables'; const PagerDutyParamsFields: React.FunctionComponent> = ({ actionParams, @@ -561,10 +394,5 @@ const PagerDutyParamsFields: React.FunctionComponent { - const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); - const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); - if (getResult !== null) { - actionTypeModel = getResult; - } -}); - -describe('actionTypeRegistry.get() works', () => { - test('action type static data is as expected', () => { - expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); - expect(actionTypeModel.iconClass).toEqual('logsApp'); - }); -}); - -describe('server-log connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.server-log', - name: 'server-log', - config: {}, - } as ActionConnector; - - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: {}, - }); - }); -}); - -describe('action params validation', () => { - test('action params validation succeeds when action params is valid', () => { - const actionParams = { - message: 'test message', - level: 'trace', - }; - - expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { message: [] }, - }); - }); -}); - -describe('ServerLogParamsFields renders', () => { - test('all params fields is rendered', () => { - expect(actionTypeModel.actionParamsFields).not.toBeNull(); - if (!actionTypeModel.actionParamsFields) { - return; - } - const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent< - ActionParamsProps - >; - const actionParams = { - level: ServerLogLevelOptions.TRACE, - message: 'test', - }; - const wrapper = mountWithIntl( - {}} - index={0} - defaultMessage={'test default message'} - /> - ); - expect(wrapper.find('[data-test-subj="loggingLevelSelect"]').length > 0).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="loggingLevelSelect"]') - .first() - .prop('value') - ).toStrictEqual('trace'); - expect(wrapper.find('[data-test-subj="loggingMessageInput"]').length > 0).toBeTruthy(); - }); - - test('level param field is rendered with default value if not selected', () => { - expect(actionTypeModel.actionParamsFields).not.toBeNull(); - if (!actionTypeModel.actionParamsFields) { - return; - } - const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent< - ActionParamsProps - >; - const actionParams = { - message: 'test message', - level: ServerLogLevelOptions.INFO, - }; - const wrapper = mountWithIntl( - {}} - index={0} - /> - ); - expect(wrapper.find('[data-test-subj="loggingLevelSelect"]').length > 0).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="loggingLevelSelect"]') - .first() - .prop('value') - ).toStrictEqual('info'); - expect(wrapper.find('[data-test-subj="loggingMessageInput"]').length > 0).toBeTruthy(); - }); - - test('params validation fails when message is not valid', () => { - const actionParams = { - message: '', - }; - - expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { - message: ['Message is required.'], - }, - }); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/index.ts new file mode 100644 index 0000000000000..f85c7460d2ece --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { getActionType as getServerLogActionType } from './server_log'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx new file mode 100644 index 0000000000000..3bb5ea68a3040 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx @@ -0,0 +1,68 @@ +/* + * 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 { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '.././index'; +import { ActionTypeModel, ActionConnector } from '../../../../types'; + +const ACTION_TYPE_ID = '.server-log'; +let actionTypeModel: ActionTypeModel; + +beforeAll(() => { + const actionTypeRegistry = new TypeRegistry(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + expect(actionTypeModel.iconClass).toEqual('logsApp'); + }); +}); + +describe('server-log connector validation', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.server-log', + name: 'server-log', + config: {}, + } as ActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: {}, + }); + }); +}); + +describe('action params validation', () => { + test('action params validation succeeds when action params is valid', () => { + const actionParams = { + message: 'test message', + level: 'trace', + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { message: [] }, + }); + }); + + test('params validation fails when message is not valid', () => { + const actionParams = { + message: '', + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + message: ['Message is required.'], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.tsx new file mode 100644 index 0000000000000..390ccf6a494e9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.tsx @@ -0,0 +1,51 @@ +/* + * 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 { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import { ActionTypeModel, ValidationResult } from '../../../../types'; +import { ServerLogActionParams } from '../types'; + +export function getActionType(): ActionTypeModel { + return { + id: '.server-log', + iconClass: 'logsApp', + selectMessage: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.selectMessageText', + { + defaultMessage: 'Add a message to a Kibana log.', + } + ), + actionTypeTitle: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.actionTypeTitle', + { + defaultMessage: 'Send to Server log', + } + ), + validateConnector: (): ValidationResult => { + return { errors: {} }; + }, + validateParams: (actionParams: ServerLogActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + message: new Array(), + }; + validationResult.errors = errors; + if (!actionParams.message?.length) { + errors.message.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServerLogMessageText', + { + defaultMessage: 'Message is required.', + } + ) + ); + } + return validationResult; + }, + actionConnectorFields: null, + actionParamsFields: lazy(() => import('./server_log_params')), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx new file mode 100644 index 0000000000000..d2e1d1e4500bc --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx @@ -0,0 +1,58 @@ +/* + * 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 from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { ServerLogLevelOptions } from '.././types'; +import ServerLogParamsFields from './server_log_params'; + +describe('ServerLogParamsFields renders', () => { + test('all params fields is rendered', () => { + const actionParams = { + level: ServerLogLevelOptions.TRACE, + message: 'test', + }; + const wrapper = mountWithIntl( + {}} + index={0} + defaultMessage={'test default message'} + /> + ); + expect(wrapper.find('[data-test-subj="loggingLevelSelect"]').length > 0).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="loggingLevelSelect"]') + .first() + .prop('value') + ).toStrictEqual('trace'); + expect(wrapper.find('[data-test-subj="loggingMessageInput"]').length > 0).toBeTruthy(); + }); + + test('level param field is rendered with default value if not selected', () => { + const actionParams = { + message: 'test message', + level: ServerLogLevelOptions.INFO, + }; + const wrapper = mountWithIntl( + {}} + index={0} + /> + ); + expect(wrapper.find('[data-test-subj="loggingLevelSelect"]').length > 0).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="loggingLevelSelect"]') + .first() + .prop('value') + ).toStrictEqual('info'); + expect(wrapper.find('[data-test-subj="loggingMessageInput"]').length > 0).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx similarity index 67% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx index a4c83ce76f04e..64d39e238be76 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx @@ -6,51 +6,9 @@ import React, { Fragment, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSelect, EuiTextArea, EuiFormRow } from '@elastic/eui'; -import { ActionTypeModel, ValidationResult, ActionParamsProps } from '../../../types'; -import { ServerLogActionParams } from './types'; -import { AddMessageVariables } from '../add_message_variables'; - -export function getActionType(): ActionTypeModel { - return { - id: '.server-log', - iconClass: 'logsApp', - selectMessage: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.selectMessageText', - { - defaultMessage: 'Add a message to a Kibana log.', - } - ), - actionTypeTitle: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.actionTypeTitle', - { - defaultMessage: 'Send to Server log', - } - ), - validateConnector: (): ValidationResult => { - return { errors: {} }; - }, - validateParams: (actionParams: ServerLogActionParams): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - message: new Array(), - }; - validationResult.errors = errors; - if (!actionParams.message?.length) { - errors.message.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServerLogMessageText', - { - defaultMessage: 'Message is required.', - } - ) - ); - } - return validationResult; - }, - actionConnectorFields: null, - actionParamsFields: ServerLogParamsFields, - }; -} +import { ActionParamsProps } from '../../../../types'; +import { ServerLogActionParams } from '.././types'; +import { AddMessageVariables } from '../../add_message_variables'; export const ServerLogParamsFields: React.FunctionComponent ); }; + +// eslint-disable-next-line import/no-default-export +export { ServerLogParamsFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.test.tsx deleted file mode 100644 index a2865b27bc06c..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.test.tsx +++ /dev/null @@ -1,166 +0,0 @@ -/* - * 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, { FunctionComponent } from 'react'; -import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { act } from 'react-dom/test-utils'; -import { TypeRegistry } from '../../type_registry'; -import { registerBuiltInActionTypes } from './index'; -import { ActionTypeModel, ActionParamsProps } from '../../../types'; -import { SlackActionParams, SlackActionConnector } from './types'; - -const ACTION_TYPE_ID = '.slack'; -let actionTypeModel: ActionTypeModel; - -let deps: any; - -beforeAll(async () => { - const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); - const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); - if (getResult !== null) { - actionTypeModel = getResult; - } - deps = { - docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, - }; -}); - -describe('actionTypeRegistry.get() works', () => { - test('action type static data is as expected', () => { - expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); - expect(actionTypeModel.iconClass).toEqual('logoSlack'); - }); -}); - -describe('slack connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { - const actionConnector = { - secrets: { - webhookUrl: 'http:\\test', - }, - id: 'test', - actionTypeId: '.email', - name: 'email', - config: {}, - } as SlackActionConnector; - - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - webhookUrl: [], - }, - }); - }); - - test('connector validation fails when connector config is not valid', () => { - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.email', - name: 'email', - config: {}, - } as SlackActionConnector; - - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - webhookUrl: ['Webhook URL is required.'], - }, - }); - }); -}); - -describe('slack action params validation', () => { - test('if action params validation succeeds when action params is valid', () => { - const actionParams = { - message: 'message {test}', - }; - - expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { message: [] }, - }); - }); -}); - -describe('SlackActionFields renders', () => { - test('all connector fields is rendered', async () => { - expect(actionTypeModel.actionConnectorFields).not.toBeNull(); - if (!actionTypeModel.actionConnectorFields) { - return; - } - const ConnectorFields = actionTypeModel.actionConnectorFields; - const actionConnector = { - secrets: { - webhookUrl: 'http:\\test', - }, - id: 'test', - actionTypeId: '.email', - name: 'email', - config: {}, - } as SlackActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - docLinks={deps!.docLinks} - /> - ); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - expect(wrapper.find('[data-test-subj="slackWebhookUrlInput"]').length > 0).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="slackWebhookUrlInput"]') - .first() - .prop('value') - ).toBe('http:\\test'); - }); -}); - -describe('SlackParamsFields renders', () => { - test('all params fields is rendered', () => { - expect(actionTypeModel.actionParamsFields).not.toBeNull(); - if (!actionTypeModel.actionParamsFields) { - return; - } - const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent< - ActionParamsProps - >; - const actionParams = { - message: 'test message', - }; - const wrapper = mountWithIntl( - {}} - index={0} - /> - ); - expect(wrapper.find('[data-test-subj="slackMessageTextArea"]').length > 0).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="slackMessageTextArea"]') - .first() - .prop('value') - ).toStrictEqual('test message'); - }); - - test('params validation fails when message is not valid', () => { - const actionParams = { - message: '', - }; - - expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { - message: ['Message is required.'], - }, - }); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.tsx deleted file mode 100644 index 03f7a2f492d54..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack.tsx +++ /dev/null @@ -1,188 +0,0 @@ -/* - * 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, useEffect } from 'react'; -import { EuiFieldText, EuiTextArea, EuiFormRow, EuiLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - ActionTypeModel, - ActionConnectorFieldsProps, - ValidationResult, - ActionParamsProps, -} from '../../../types'; -import { SlackActionParams, SlackActionConnector } from './types'; -import { AddMessageVariables } from '../add_message_variables'; - -export function getActionType(): ActionTypeModel { - return { - id: '.slack', - iconClass: 'logoSlack', - selectMessage: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.selectMessageText', - { - defaultMessage: 'Send a message to a Slack channel or user.', - } - ), - actionTypeTitle: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle', - { - defaultMessage: 'Send to Slack', - } - ), - validateConnector: (action: SlackActionConnector): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - webhookUrl: new Array(), - }; - validationResult.errors = errors; - if (!action.secrets.webhookUrl) { - errors.webhookUrl.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText', - { - defaultMessage: 'Webhook URL is required.', - } - ) - ); - } - return validationResult; - }, - validateParams: (actionParams: SlackActionParams): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - message: new Array(), - }; - validationResult.errors = errors; - if (!actionParams.message?.length) { - errors.message.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSlackMessageText', - { - defaultMessage: 'Message is required.', - } - ) - ); - } - return validationResult; - }, - actionConnectorFields: SlackActionFields, - actionParamsFields: SlackParamsFields, - }; -} - -const SlackActionFields: React.FunctionComponent> = ({ action, editActionSecrets, errors, docLinks }) => { - const { webhookUrl } = action.secrets; - - return ( - - - - - } - error={errors.webhookUrl} - isInvalid={errors.webhookUrl.length > 0 && webhookUrl !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel', - { - defaultMessage: 'Webhook URL', - } - )} - > - 0 && webhookUrl !== undefined} - name="webhookUrl" - placeholder="Example: https://hooks.slack.com/services" - value={webhookUrl || ''} - data-test-subj="slackWebhookUrlInput" - onChange={e => { - editActionSecrets('webhookUrl', e.target.value); - }} - onBlur={() => { - if (!webhookUrl) { - editActionSecrets('webhookUrl', ''); - } - }} - /> - - - ); -}; - -const SlackParamsFields: React.FunctionComponent> = ({ - actionParams, - editAction, - index, - errors, - messageVariables, - defaultMessage, -}) => { - const { message } = actionParams; - useEffect(() => { - if (!message && defaultMessage && defaultMessage.length > 0) { - editAction('message', defaultMessage, index); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const onSelectMessageVariable = (paramsProperty: string, variable: string) => { - editAction(paramsProperty, (message ?? '').concat(` {{${variable}}}`), index); - }; - - return ( - - 0 && message !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.messageTextAreaFieldLabel', - { - defaultMessage: 'Message', - } - )} - labelAppend={ - - onSelectMessageVariable('message', variable) - } - paramsProperty="message" - /> - } - > - 0 && message !== undefined} - name="message" - value={message || ''} - data-test-subj="slackMessageTextArea" - onChange={e => { - editAction('message', e.target.value, index); - }} - onBlur={() => { - if (!message) { - editAction('message', '', index); - } - }} - /> - - - ); -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/index.ts new file mode 100644 index 0000000000000..64ab6670754c9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { getActionType as getSlackActionType } from './slack'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.test.tsx new file mode 100644 index 0000000000000..78f4161cac827 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.test.tsx @@ -0,0 +1,88 @@ +/* + * 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 { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '.././index'; +import { ActionTypeModel } from '../../../../types'; +import { SlackActionConnector } from '../types'; + +const ACTION_TYPE_ID = '.slack'; +let actionTypeModel: ActionTypeModel; + +beforeAll(async () => { + const actionTypeRegistry = new TypeRegistry(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + expect(actionTypeModel.iconClass).toEqual('logoSlack'); + }); +}); + +describe('slack connector validation', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: { + webhookUrl: 'http:\\test', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: {}, + } as SlackActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + webhookUrl: [], + }, + }); + }); + + test('connector validation fails when connector config is not valid', () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: {}, + } as SlackActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + webhookUrl: ['Webhook URL is required.'], + }, + }); + }); +}); + +describe('slack action params validation', () => { + test('if action params validation succeeds when action params is valid', () => { + const actionParams = { + message: 'message {test}', + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { message: [] }, + }); + }); + + test('params validation fails when message is not valid', () => { + const actionParams = { + message: '', + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + message: ['Message is required.'], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.tsx new file mode 100644 index 0000000000000..5d39cdb5ac387 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.tsx @@ -0,0 +1,66 @@ +/* + * 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 { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import { ActionTypeModel, ValidationResult } from '../../../../types'; +import { SlackActionParams, SlackActionConnector } from '../types'; + +export function getActionType(): ActionTypeModel { + return { + id: '.slack', + iconClass: 'logoSlack', + selectMessage: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.selectMessageText', + { + defaultMessage: 'Send a message to a Slack channel or user.', + } + ), + actionTypeTitle: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle', + { + defaultMessage: 'Send to Slack', + } + ), + validateConnector: (action: SlackActionConnector): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + webhookUrl: new Array(), + }; + validationResult.errors = errors; + if (!action.secrets.webhookUrl) { + errors.webhookUrl.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText', + { + defaultMessage: 'Webhook URL is required.', + } + ) + ); + } + return validationResult; + }, + validateParams: (actionParams: SlackActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + message: new Array(), + }; + validationResult.errors = errors; + if (!actionParams.message?.length) { + errors.message.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSlackMessageText', + { + defaultMessage: 'Message is required.', + } + ) + ); + } + return validationResult; + }, + actionConnectorFields: lazy(() => import('./slack_connectors')), + actionParamsFields: lazy(() => import('./slack_params')), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx new file mode 100644 index 0000000000000..7d7f6fc086928 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx @@ -0,0 +1,49 @@ +/* + * 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 from 'react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { act } from '@testing-library/react'; +import { SlackActionConnector } from '../types'; +import SlackActionFields from './slack_connectors'; +import { DocLinksStart } from 'kibana/public'; + +describe('SlackActionFields renders', () => { + test('all connector fields is rendered', async () => { + const actionConnector = { + secrets: { + webhookUrl: 'http:\\test', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: {}, + } as SlackActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + expect(wrapper.find('[data-test-subj="slackWebhookUrlInput"]').length > 0).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="slackWebhookUrlInput"]') + .first() + .prop('value') + ).toBe('http:\\test'); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx new file mode 100644 index 0000000000000..ad3e76ad8ae6c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx @@ -0,0 +1,65 @@ +/* + * 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 } from 'react'; +import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import { SlackActionConnector } from '../types'; + +const SlackActionFields: React.FunctionComponent> = ({ action, editActionSecrets, errors, docLinks }) => { + const { webhookUrl } = action.secrets; + + return ( + + + + + } + error={errors.webhookUrl} + isInvalid={errors.webhookUrl.length > 0 && webhookUrl !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel', + { + defaultMessage: 'Webhook URL', + } + )} + > + 0 && webhookUrl !== undefined} + name="webhookUrl" + placeholder="Example: https://hooks.slack.com/services" + value={webhookUrl || ''} + data-test-subj="slackWebhookUrlInput" + onChange={e => { + editActionSecrets('webhookUrl', e.target.value); + }} + onBlur={() => { + if (!webhookUrl) { + editActionSecrets('webhookUrl', ''); + } + }} + /> + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SlackActionFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx new file mode 100644 index 0000000000000..4183aeb48dec7 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx @@ -0,0 +1,31 @@ +/* + * 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 from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import SlackParamsFields from './slack_params'; + +describe('SlackParamsFields renders', () => { + test('all params fields is rendered', () => { + const actionParams = { + message: 'test message', + }; + const wrapper = mountWithIntl( + {}} + index={0} + /> + ); + expect(wrapper.find('[data-test-subj="slackMessageTextArea"]').length > 0).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="slackMessageTextArea"]') + .first() + .prop('value') + ).toStrictEqual('test message'); + }); +}); 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 new file mode 100644 index 0000000000000..42fefdd41ef67 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.tsx @@ -0,0 +1,77 @@ +/* + * 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, useEffect } from 'react'; +import { EuiTextArea, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionParamsProps } from '../../../../types'; +import { SlackActionParams } from '../types'; +import { AddMessageVariables } from '../../add_message_variables'; + +const SlackParamsFields: React.FunctionComponent> = ({ + actionParams, + editAction, + index, + errors, + messageVariables, + defaultMessage, +}) => { + const { message } = actionParams; + useEffect(() => { + if (!message && defaultMessage && defaultMessage.length > 0) { + editAction('message', defaultMessage, index); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const onSelectMessageVariable = (paramsProperty: string, variable: string) => { + editAction(paramsProperty, (message ?? '').concat(` {{${variable}}}`), index); + }; + + return ( + + 0 && message !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.messageTextAreaFieldLabel', + { + defaultMessage: 'Message', + } + )} + labelAppend={ + + onSelectMessageVariable('message', variable) + } + paramsProperty="message" + /> + } + > + 0 && message !== undefined} + name="message" + value={message || ''} + data-test-subj="slackMessageTextArea" + onChange={e => { + editAction('message', e.target.value, index); + }} + onBlur={() => { + if (!message) { + editAction('message', '', index); + } + }} + /> + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SlackParamsFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx deleted file mode 100644 index 7d0082708075f..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.test.tsx +++ /dev/null @@ -1,179 +0,0 @@ -/* - * 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, { FunctionComponent } from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { TypeRegistry } from '../../type_registry'; -import { registerBuiltInActionTypes } from './index'; -import { ActionTypeModel, ActionParamsProps } from '../../../types'; -import { WebhookActionParams, WebhookActionConnector } from './types'; - -const ACTION_TYPE_ID = '.webhook'; -let actionTypeModel: ActionTypeModel; - -beforeAll(() => { - const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); - const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); - if (getResult !== null) { - actionTypeModel = getResult; - } -}); - -describe('actionTypeRegistry.get() works', () => { - test('action type static data is as expected', () => { - expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); - expect(actionTypeModel.iconClass).toEqual('logoWebhook'); - }); -}); - -describe('webhook connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { - const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.webhook', - name: 'webhook', - isPreconfigured: false, - config: { - method: 'PUT', - url: 'http:\\test', - headers: { 'content-type': 'text' }, - }, - } as WebhookActionConnector; - - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - url: [], - method: [], - user: [], - password: [], - }, - }); - }); - - test('connector validation fails when connector config is not valid', () => { - const actionConnector = { - secrets: { - user: 'user', - }, - id: 'test', - actionTypeId: '.webhook', - name: 'webhook', - config: { - method: 'PUT', - }, - } as WebhookActionConnector; - - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - url: ['URL is required.'], - method: [], - user: [], - password: ['Password is required.'], - }, - }); - }); -}); - -describe('webhook action params validation', () => { - test('action params validation succeeds when action params is valid', () => { - const actionParams = { - body: 'message {test}', - }; - - expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { body: [] }, - }); - }); -}); - -describe('WebhookActionConnectorFields renders', () => { - test('all connector fields is rendered', () => { - expect(actionTypeModel.actionConnectorFields).not.toBeNull(); - if (!actionTypeModel.actionConnectorFields) { - return; - } - const ConnectorFields = actionTypeModel.actionConnectorFields; - const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.webhook', - isPreconfigured: false, - name: 'webhook', - config: { - method: 'PUT', - url: 'http:\\test', - headers: { 'content-type': 'text' }, - }, - } as WebhookActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - /> - ); - expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); - wrapper - .find('[data-test-subj="webhookViewHeadersSwitch"]') - .first() - .simulate('click'); - expect(wrapper.find('[data-test-subj="webhookMethodSelect"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookUrlText"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookUserInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookPasswordInput"]').length > 0).toBeTruthy(); - }); -}); - -describe('WebhookParamsFields renders', () => { - test('all params fields is rendered', () => { - expect(actionTypeModel.actionParamsFields).not.toBeNull(); - if (!actionTypeModel.actionParamsFields) { - return; - } - const ParamsFields = actionTypeModel.actionParamsFields as FunctionComponent< - ActionParamsProps - >; - const actionParams = { - body: 'test message', - }; - const wrapper = mountWithIntl( - {}} - index={0} - /> - ); - expect(wrapper.find('[data-test-subj="webhookBodyEditor"]').length > 0).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="webhookBodyEditor"]') - .first() - .prop('value') - ).toStrictEqual('test message'); - expect(wrapper.find('[data-test-subj="bodyAddVariableButton"]').length > 0).toBeTruthy(); - }); - - test('params validation fails when body is not valid', () => { - const actionParams = { - body: '', - }; - - expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { - body: ['Body is required.'], - }, - }); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/index.ts new file mode 100644 index 0000000000000..c43cab26b072e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { getActionType as getWebhookActionType } from './webhook'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx new file mode 100644 index 0000000000000..3413465d70d93 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx @@ -0,0 +1,104 @@ +/* + * 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 { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '.././index'; +import { ActionTypeModel } from '../../../../types'; +import { WebhookActionConnector } from '../types'; + +const ACTION_TYPE_ID = '.webhook'; +let actionTypeModel: ActionTypeModel; + +beforeAll(() => { + const actionTypeRegistry = new TypeRegistry(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + expect(actionTypeModel.iconClass).toEqual('logoWebhook'); + }); +}); + +describe('webhook connector validation', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.webhook', + name: 'webhook', + isPreconfigured: false, + config: { + method: 'PUT', + url: 'http:\\test', + headers: { 'content-type': 'text' }, + }, + } as WebhookActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + url: [], + method: [], + user: [], + password: [], + }, + }); + }); + + test('connector validation fails when connector config is not valid', () => { + const actionConnector = { + secrets: { + user: 'user', + }, + id: 'test', + actionTypeId: '.webhook', + name: 'webhook', + config: { + method: 'PUT', + }, + } as WebhookActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + url: ['URL is required.'], + method: [], + user: [], + password: ['Password is required.'], + }, + }); + }); +}); + +describe('webhook action params validation', () => { + test('action params validation succeeds when action params is valid', () => { + const actionParams = { + body: 'message {test}', + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { body: [] }, + }); + }); + + test('params validation fails when body is not valid', () => { + const actionParams = { + body: '', + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + body: ['Body is required.'], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx new file mode 100644 index 0000000000000..9f33e4491233a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx @@ -0,0 +1,99 @@ +/* + * 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 { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import { ActionTypeModel, ValidationResult } from '../../../../types'; +import { WebhookActionParams, WebhookActionConnector } from '../types'; + +export function getActionType(): ActionTypeModel { + return { + id: '.webhook', + iconClass: 'logoWebhook', + selectMessage: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.selectMessageText', + { + defaultMessage: 'Send a request to a web service.', + } + ), + actionTypeTitle: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.actionTypeTitle', + { + defaultMessage: 'Webhook data', + } + ), + validateConnector: (action: WebhookActionConnector): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + url: new Array(), + method: new Array(), + user: new Array(), + password: new Array(), + }; + validationResult.errors = errors; + if (!action.config.url) { + errors.url.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.requiredUrlText', + { + defaultMessage: 'URL is required.', + } + ) + ); + } + if (!action.config.method) { + errors.method.push( + i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText', + { + defaultMessage: 'Method is required.', + } + ) + ); + } + if (!action.secrets.user && action.secrets.password) { + errors.user.push( + i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHostText', + { + defaultMessage: 'Username is required.', + } + ) + ); + } + if (!action.secrets.password && action.secrets.user) { + errors.password.push( + i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText', + { + defaultMessage: 'Password is required.', + } + ) + ); + } + return validationResult; + }, + validateParams: (actionParams: WebhookActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + body: new Array(), + }; + validationResult.errors = errors; + if (!actionParams.body?.length) { + errors.body.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText', + { + defaultMessage: 'Body is required.', + } + ) + ); + } + return validationResult; + }, + actionConnectorFields: lazy(() => import('./webhook_connectors')), + actionParamsFields: lazy(() => import('./webhook_params')), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx new file mode 100644 index 0000000000000..842ec51785355 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx @@ -0,0 +1,48 @@ +/* + * 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 from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { WebhookActionConnector } from '../types'; +import WebhookActionConnectorFields from './webhook_connectors'; +import { DocLinksStart } from 'kibana/public'; + +describe('WebhookActionConnectorFields renders', () => { + test('all connector fields is rendered', () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.webhook', + isPreconfigured: false, + name: 'webhook', + config: { + method: 'PUT', + url: 'http:\\test', + headers: { 'content-type': 'text' }, + }, + } as WebhookActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + /> + ); + expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); + wrapper + .find('[data-test-subj="webhookViewHeadersSwitch"]') + .first() + .simulate('click'); + expect(wrapper.find('[data-test-subj="webhookMethodSelect"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookUrlText"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookUserInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookPasswordInput"]').length > 0).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx similarity index 71% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx index daa5a6caeabe9..e163463602d9f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx @@ -19,112 +19,15 @@ import { EuiDescriptionListDescription, EuiDescriptionListTitle, EuiTitle, - EuiCodeEditor, EuiSwitch, EuiButtonEmpty, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { - ActionTypeModel, - ActionConnectorFieldsProps, - ValidationResult, - ActionParamsProps, -} from '../../../types'; -import { WebhookActionParams, WebhookActionConnector } from './types'; -import { AddMessageVariables } from '../add_message_variables'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import { WebhookActionConnector } from '../types'; const HTTP_VERBS = ['post', 'put']; -export function getActionType(): ActionTypeModel { - return { - id: '.webhook', - iconClass: 'logoWebhook', - selectMessage: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.selectMessageText', - { - defaultMessage: 'Send a request to a web service.', - } - ), - actionTypeTitle: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.actionTypeTitle', - { - defaultMessage: 'Webhook data', - } - ), - validateConnector: (action: WebhookActionConnector): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - url: new Array(), - method: new Array(), - user: new Array(), - password: new Array(), - }; - validationResult.errors = errors; - if (!action.config.url) { - errors.url.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.requiredUrlText', - { - defaultMessage: 'URL is required.', - } - ) - ); - } - if (!action.config.method) { - errors.method.push( - i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText', - { - defaultMessage: 'Method is required.', - } - ) - ); - } - if (!action.secrets.user && action.secrets.password) { - errors.user.push( - i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHostText', - { - defaultMessage: 'Username is required.', - } - ) - ); - } - if (!action.secrets.password && action.secrets.user) { - errors.password.push( - i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText', - { - defaultMessage: 'Password is required.', - } - ) - ); - } - return validationResult; - }, - validateParams: (actionParams: WebhookActionParams): ValidationResult => { - const validationResult = { errors: {} }; - const errors = { - body: new Array(), - }; - validationResult.errors = errors; - if (!actionParams.body?.length) { - errors.body.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText', - { - defaultMessage: 'Body is required.', - } - ) - ); - } - return validationResult; - }, - actionConnectorFields: WebhookActionConnectorFields, - actionParamsFields: WebhookParamsFields, - }; -} - const WebhookActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors }) => { @@ -457,56 +360,5 @@ const WebhookActionConnectorFields: React.FunctionComponent> = ({ - actionParams, - editAction, - index, - messageVariables, - errors, -}) => { - const { body } = actionParams; - const onSelectMessageVariable = (paramsProperty: string, variable: string) => { - editAction(paramsProperty, (body ?? '').concat(` {{${variable}}}`), index); - }; - return ( - - 0 && body !== undefined} - fullWidth - error={errors.body} - labelAppend={ - onSelectMessageVariable('body', variable)} - paramsProperty="body" - /> - } - > - { - editAction('body', json, index); - }} - /> - - - ); -}; +// eslint-disable-next-line import/no-default-export +export { WebhookActionConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx new file mode 100644 index 0000000000000..5ca27a53083f9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx @@ -0,0 +1,32 @@ +/* + * 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 from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import WebhookParamsFields from './webhook_params'; + +describe('WebhookParamsFields renders', () => { + test('all params fields is rendered', () => { + const actionParams = { + body: 'test message', + }; + const wrapper = mountWithIntl( + {}} + index={0} + /> + ); + expect(wrapper.find('[data-test-subj="webhookBodyEditor"]').length > 0).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="webhookBodyEditor"]') + .first() + .prop('value') + ).toStrictEqual('test message'); + expect(wrapper.find('[data-test-subj="bodyAddVariableButton"]').length > 0).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx new file mode 100644 index 0000000000000..9e802b96e16be --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx @@ -0,0 +1,68 @@ +/* + * 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 } from 'react'; +import { EuiFormRow, EuiCodeEditor } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionParamsProps } from '../../../../types'; +import { WebhookActionParams } from '../types'; +import { AddMessageVariables } from '../../add_message_variables'; + +const WebhookParamsFields: React.FunctionComponent> = ({ + actionParams, + editAction, + index, + messageVariables, + errors, +}) => { + const { body } = actionParams; + const onSelectMessageVariable = (paramsProperty: string, variable: string) => { + editAction(paramsProperty, (body ?? '').concat(` {{${variable}}}`), index); + }; + return ( + + 0 && body !== undefined} + fullWidth + error={errors.body} + labelAppend={ + onSelectMessageVariable('body', variable)} + paramsProperty="body" + /> + } + > + { + editAction('body', json, index); + }} + /> + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { WebhookParamsFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx index 4d0a9980f2231..b5f3b63c58a93 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx @@ -167,3 +167,6 @@ export const TriggersActionsUIHome: React.FunctionComponent ); }; + +// eslint-disable-next-line import/no-default-export +export { TriggersActionsUIHome as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx index 6bb8a8f4e4c10..06ddce39567a4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx @@ -3,7 +3,7 @@ * 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 } from 'react'; +import React, { Fragment, Suspense } from 'react'; import { EuiForm, EuiCallOut, @@ -12,6 +12,9 @@ import { EuiSpacer, EuiFieldText, EuiFormRow, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -151,14 +154,24 @@ export const ActionConnectorForm = ({ {FieldsComponent !== null ? ( - + + + + + + } + > + + ) : null} ); 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 6935dda358d9c..ae179f56f0c83 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useState, useEffect } from 'react'; +import React, { Fragment, Suspense, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -27,6 +27,7 @@ import { EuiCallOut, EuiHorizontalRule, EuiText, + EuiLoadingSpinner, } from '@elastic/eui'; import { HttpSetup, ToastsApi, ApplicationStart, DocLinksStart } from 'kibana/public'; import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/action_connector_api'; @@ -282,14 +283,24 @@ export const ActionForm = ({ {ParamsFieldsComponent ? ( - + + + + + + } + > + + ) : null} ) : ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx index 9198607df7863..0caa880c4df00 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx @@ -118,6 +118,6 @@ export async function getAlertData( } } -export const AlertDetailsRouteWithApi = withActionOperations( - withBulkAlertOperations(AlertDetailsRoute) -); +const AlertDetailsRouteWithApi = withActionOperations(withBulkAlertOperations(AlertDetailsRoute)); +// eslint-disable-next-line import/no-default-export +export { AlertDetailsRouteWithApi as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts index 93e61cf5b4f43..62173a6196b98 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts @@ -23,7 +23,11 @@ const getTestAlertType = (id?: string, name?: string, iconClass?: string) => { }; }; -const getTestActionType = (id?: string, iconClass?: string, selectedMessage?: string) => { +const getTestActionType = ( + id?: string, + iconClass?: string, + selectedMessage?: string +): ActionTypeModel => { return { id: id || 'my-action-type', iconClass: iconClass || 'test', diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 6f33bcb8b226d..cc511434267cc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { HttpSetup, DocLinksStart } from 'kibana/public'; +import { ComponentType } from 'react'; import { ActionGroup } from '../../alerting/common'; import { ActionType } from '../../actions/common'; import { TypeRegistry } from './application/type_registry'; @@ -19,14 +20,16 @@ export { ActionType }; export type ActionTypeIndex = Record; export type AlertTypeIndex = Record; -export type ActionTypeRegistryContract = PublicMethodsOf>; +export type ActionTypeRegistryContract = PublicMethodsOf< + TypeRegistry> +>; export type AlertTypeRegistryContract = PublicMethodsOf>; export interface ActionConnectorFieldsProps { action: TActionConnector; editActionConfig: (property: string, value: any) => void; editActionSecrets: (property: string, value: any) => void; - errors: { [key: string]: string[] }; + errors: IErrorObject; docLinks: DocLinksStart; http?: HttpSetup; } @@ -35,7 +38,7 @@ export interface ActionParamsProps { actionParams: TParams; index: number; editAction: (property: string, value: any, index: number) => void; - errors: { [key: string]: string[] }; + errors: IErrorObject; messageVariables?: string[]; defaultMessage?: string; } @@ -45,15 +48,19 @@ export interface Pagination { size: number; } -export interface ActionTypeModel { +export interface ActionTypeModel { id: string; iconClass: string; selectMessage: string; actionTypeTitle?: string; validateConnector: (connector: any) => ValidationResult; validateParams: (actionParams: any) => ValidationResult; - actionConnectorFields: React.FunctionComponent | null; - actionParamsFields: any; + actionConnectorFields: React.LazyExoticComponent< + ComponentType> + > | null; + actionParamsFields: React.LazyExoticComponent< + ComponentType> + > | null; } export interface ValidationResult {