diff --git a/x-pack/plugins/cases/public/components/connectors/case/index.ts b/x-pack/plugins/cases/public/components/connectors/case/index.ts index c2cf4980da7e..8e6680cd6538 100644 --- a/x-pack/plugins/cases/public/components/connectors/case/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/case/index.ts @@ -25,7 +25,7 @@ const validateParams = (actionParams: CaseActionParams) => { validationResult.errors.caseId.push(i18n.CASE_CONNECTOR_CASE_REQUIRED); } - return validationResult; + return Promise.resolve(validationResult); }; export function getActionType(): ActionTypeModel { @@ -34,7 +34,7 @@ export function getActionType(): ActionTypeModel { iconClass: 'securityAnalyticsApp', selectMessage: i18n.CASE_CONNECTOR_DESC, actionTypeTitle: i18n.CASE_CONNECTOR_TITLE, - validateConnector: () => ({ config: { errors: {} }, secrets: { errors: {} } }), + validateConnector: () => Promise.resolve({ config: { errors: {} }, secrets: { errors: {} } }), validateParams, actionConnectorFields: null, actionParamsFields: lazy(() => import('./alert_fields')), diff --git a/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx b/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx index 11642f4083d3..3eda13f5bcb3 100644 --- a/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx +++ b/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx @@ -94,12 +94,12 @@ describe('alert_form', () => { id: 'alert-action-type', iconClass: '', selectMessage: '', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, actionParamsFields: mockedActionParamsFields, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx index a31371c31cbb..8a85d35d77fa 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx @@ -89,6 +89,7 @@ const StepRuleActionsComponent: FC = ({ ...(defaultValues ?? stepActionsDefaultValue), kibanaSiemAppUrl: kibanaAbsoluteUrl, }; + const schema = useMemo(() => getSchema({ actionTypeRegistry }), [actionTypeRegistry]); const { form } = useForm({ defaultValue: initialState, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.test.tsx index 992f30e795bb..3266d6f61eee 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.test.tsx @@ -15,13 +15,13 @@ describe('stepRuleActions schema', () => { const actionTypeRegistry = actionTypeRegistryMock.create(); describe('validateSingleAction', () => { - it('should validate single action', () => { + it('should validate single action', async () => { (isUuid as jest.Mock).mockReturnValue(true); (validateActionParams as jest.Mock).mockReturnValue([]); (validateMustache as jest.Mock).mockReturnValue([]); expect( - validateSingleAction( + await validateSingleAction( { id: '817b8bca-91d1-4729-8ee1-3a83aaafd9d4', group: 'default', @@ -33,12 +33,12 @@ describe('stepRuleActions schema', () => { ).toHaveLength(0); }); - it('should validate single action with invalid mustache template', () => { + it('should validate single action with invalid mustache template', async () => { (isUuid as jest.Mock).mockReturnValue(true); (validateActionParams as jest.Mock).mockReturnValue([]); (validateMustache as jest.Mock).mockReturnValue(['Message is not valid mustache template']); - const errors = validateSingleAction( + const errors = await validateSingleAction( { id: '817b8bca-91d1-4729-8ee1-3a83aaafd9d4', group: 'default', @@ -54,12 +54,12 @@ describe('stepRuleActions schema', () => { expect(errors[0]).toEqual('Message is not valid mustache template'); }); - it('should validate single action with incorrect id', () => { + it('should validate single action with incorrect id', async () => { (isUuid as jest.Mock).mockReturnValue(false); (validateMustache as jest.Mock).mockReturnValue([]); (validateActionParams as jest.Mock).mockReturnValue([]); - const errors = validateSingleAction( + const errors = await validateSingleAction( { id: '823d4', group: 'default', @@ -74,10 +74,10 @@ describe('stepRuleActions schema', () => { }); describe('validateRuleActionsField', () => { - it('should validate rule actions field', () => { + it('should validate rule actions field', async () => { const validator = validateRuleActionsField(actionTypeRegistry); - const result = validator({ + const result = await validator({ path: '', value: [], form: {} as FormHook, @@ -88,11 +88,11 @@ describe('stepRuleActions schema', () => { expect(result).toEqual(undefined); }); - it('should validate incorrect rule actions field', () => { + it('should validate incorrect rule actions field', async () => { (getActionTypeName as jest.Mock).mockReturnValue('Slack'); const validator = validateRuleActionsField(actionTypeRegistry); - const result = validator({ + const result = await validator({ path: '', value: [ { @@ -117,7 +117,7 @@ describe('stepRuleActions schema', () => { }); }); - it('should validate multiple incorrect rule actions field', () => { + it('should validate multiple incorrect rule actions field', async () => { (isUuid as jest.Mock).mockReturnValueOnce(false); (getActionTypeName as jest.Mock).mockReturnValueOnce('Slack'); (isUuid as jest.Mock).mockReturnValueOnce(true); @@ -126,7 +126,7 @@ describe('stepRuleActions schema', () => { (validateMustache as jest.Mock).mockReturnValue(['Component is not valid mustache template']); const validator = validateRuleActionsField(actionTypeRegistry); - const result = validator({ + const result = await validator({ path: '', value: [ { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx index bc32bdc387cd..a697d922eda9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx @@ -13,42 +13,46 @@ import { AlertAction, ActionTypeRegistryContract, } from '../../../../../../triggers_actions_ui/public'; -import { FormSchema, ValidationFunc, ERROR_CODE } from '../../../../shared_imports'; +import { + FormSchema, + ValidationFunc, + ERROR_CODE, + ValidationError, +} from '../../../../shared_imports'; import { ActionsStepRule } from '../../../pages/detection_engine/rules/types'; import * as I18n from './translations'; import { isUuid, getActionTypeName, validateMustache, validateActionParams } from './utils'; -export const validateSingleAction = ( +export const validateSingleAction = async ( actionItem: AlertAction, actionTypeRegistry: ActionTypeRegistryContract -): string[] => { +): Promise => { if (!isUuid(actionItem.id)) { return [I18n.NO_CONNECTOR_SELECTED]; } - const actionParamsErrors = validateActionParams(actionItem, actionTypeRegistry); + const actionParamsErrors = await validateActionParams(actionItem, actionTypeRegistry); const mustacheErrors = validateMustache(actionItem.params); return [...actionParamsErrors, ...mustacheErrors]; }; -export const validateRuleActionsField = (actionTypeRegistry: ActionTypeRegistryContract) => ( +export const validateRuleActionsField = (actionTypeRegistry: ActionTypeRegistryContract) => async ( ...data: Parameters -): ReturnType> | undefined => { +): Promise | void | undefined> => { const [{ value, path }] = data as [{ value: AlertAction[]; path: string }]; - const errors = value.reduce((acc, actionItem) => { - const errorsArray = validateSingleAction(actionItem, actionTypeRegistry); + const errors = []; + for (const actionItem of value) { + const errorsArray = await validateSingleAction(actionItem, actionTypeRegistry); if (errorsArray.length) { const actionTypeName = getActionTypeName(actionItem.actionTypeId); const errorsListItems = errorsArray.map((error) => `* ${error}\n`); - return [...acc, `\n**${actionTypeName}:**\n${errorsListItems.join('')}`]; + errors.push(`\n**${actionTypeName}:**\n${errorsListItems.join('')}`); } - - return acc; - }, [] as string[]); + } if (errors.length) { return { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.test.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.test.ts index 3d7299c1673b..7c4ea71c983c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.test.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.test.ts @@ -61,11 +61,11 @@ describe('stepRuleActions utils', () => { actionTypeRegistry.get.mockReturnValue(actionMock); }); - it('should validate action params', () => { + it('should validate action params', async () => { validateParamsMock.mockReturnValue({ errors: [] }); expect( - validateActionParams( + await validateActionParams( { id: '817b8bca-91d1-4729-8ee1-3a83aaafd9d4', group: 'default', @@ -79,13 +79,13 @@ describe('stepRuleActions utils', () => { ).toHaveLength(0); }); - it('should validate incorrect action params', () => { + it('should validate incorrect action params', async () => { validateParamsMock.mockReturnValue({ errors: ['Message is required'], }); expect( - validateActionParams( + await validateActionParams( { id: '817b8bca-91d1-4729-8ee1-3a83aaafd9d4', group: 'default', @@ -97,7 +97,7 @@ describe('stepRuleActions utils', () => { ).toHaveLength(1); }); - it('should validate incorrect action params and filter error objects', () => { + it('should validate incorrect action params and filter error objects', async () => { validateParamsMock.mockReturnValue({ errors: [ { @@ -107,7 +107,7 @@ describe('stepRuleActions utils', () => { }); expect( - validateActionParams( + await validateActionParams( { id: '817b8bca-91d1-4729-8ee1-3a83aaafd9d4', group: 'default', @@ -119,13 +119,13 @@ describe('stepRuleActions utils', () => { ).toHaveLength(0); }); - it('should validate incorrect action params and filter duplicated errors', () => { + it('should validate incorrect action params and filter duplicated errors', async () => { validateParamsMock.mockReturnValue({ errors: ['Message is required', 'Message is required', 'Message is required'], }); expect( - validateActionParams( + await validateActionParams( { id: '817b8bca-91d1-4729-8ee1-3a83aaafd9d4', group: 'default', diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.ts index d241d4283fc7..22363df5164a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.ts @@ -41,11 +41,11 @@ export const validateMustache = (params: AlertAction['params']) => { return errors; }; -export const validateActionParams = ( +export const validateActionParams = async ( actionItem: AlertAction, actionTypeRegistry: ActionTypeRegistryContract -): string[] => { - const actionErrors = actionTypeRegistry +): Promise => { + const actionErrors = await actionTypeRegistry .get(actionItem.actionTypeId) ?.validateParams(actionItem.params); diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index 7d736218af2d..cd83be0138fa 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -888,10 +888,10 @@ export function getActionType(): ActionTypeModel { defaultMessage: 'Send to Server log', } ), - validateConnector: (): ValidationResult => { + validateConnector: (): Promise => { return { errors: {} }; }, - validateParams: (actionParams: ServerLogActionParams): ValidationResult => { + validateParams: (actionParams: ServerLogActionParams): Promise => { // validation of action params implementation }, actionConnectorFields: null, @@ -929,10 +929,10 @@ export function getActionType(): ActionTypeModel { defaultMessage: 'Send to email', } ), - validateConnector: (action: EmailActionConnector): ValidationResult => { + validateConnector: (action: EmailActionConnector): Promise => { // validation of connector properties implementation }, - validateParams: (actionParams: EmailActionParams): ValidationResult => { + validateParams: (actionParams: EmailActionParams): Promise => { // validation of action params implementation }, actionConnectorFields: EmailActionConnectorFields, @@ -967,10 +967,10 @@ export function getActionType(): ActionTypeModel { defaultMessage: 'Send to Slack', } ), - validateConnector: (action: SlackActionConnector): ValidationResult => { + validateConnector: (action: SlackActionConnector): Promise => { // validation of connector properties implementation }, - validateParams: (actionParams: SlackActionParams): ValidationResult => { + validateParams: (actionParams: SlackActionParams): Promise => { // validation of action params implementation }, actionConnectorFields: SlackActionFields, @@ -1000,12 +1000,12 @@ export function getActionType(): ActionTypeModel { defaultMessage: 'Index data into Elasticsearch.', } ), - validateConnector: (): ValidationResult => { + validateConnector: (): Promise => { return { errors: {} }; }, actionConnectorFields: IndexActionConnectorFields, actionParamsFields: IndexParamsFields, - validateParams: (): ValidationResult => { + validateParams: (): Promise => { return { errors: {} }; }, }; @@ -1046,10 +1046,10 @@ export function getActionType(): ActionTypeModel { defaultMessage: 'Send a request to a web service.', } ), - validateConnector: (action: WebhookActionConnector): ValidationResult => { + validateConnector: (action: WebhookActionConnector): Promise => { // validation of connector properties implementation }, - validateParams: (actionParams: WebhookActionParams): ValidationResult => { + validateParams: (actionParams: WebhookActionParams): Promise => { // validation of action params implementation }, actionConnectorFields: WebhookActionConnectorFields, @@ -1086,10 +1086,10 @@ export function getActionType(): ActionTypeModel { defaultMessage: 'Send to PagerDuty', } ), - validateConnector: (action: PagerDutyActionConnector): ValidationResult => { + validateConnector: (action: PagerDutyActionConnector): Promise => { // validation of connector properties implementation }, - validateParams: (actionParams: PagerDutyActionParams): ValidationResult => { + validateParams: (actionParams: PagerDutyActionParams): Promise => { // validation of action params implementation }, actionConnectorFields: PagerDutyActionConnectorFields, @@ -1113,8 +1113,8 @@ Each action type should be defined as an `ActionTypeModel` object with the follo iconClass: IconType; selectMessage: string; actionTypeTitle?: string; - validateConnector: (connector: any) => ValidationResult; - validateParams: (actionParams: any) => ValidationResult; + validateConnector: (connector: any) => Promise; + validateParams: (actionParams: any) => Promise; actionConnectorFields: React.FunctionComponent | null; actionParamsFields: React.LazyExoticComponent>>; ``` @@ -1186,7 +1186,7 @@ export function getActionType(): ActionTypeModel { defaultMessage: 'Example Action', } ), - validateConnector: (action: ExampleActionConnector): ValidationResult => { + validateConnector: (action: ExampleActionConnector): Promise => { const validationResult = { errors: {} }; const errors = { someConnectorField: new Array(), @@ -1204,7 +1204,7 @@ export function getActionType(): ActionTypeModel { } return validationResult; }, - validateParams: (actionParams: ExampleActionParams): ValidationResult => { + validateParams: (actionParams: ExampleActionParams): Promise => { const validationResult = { errors: {} }; const errors = { message: new Array(), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx index bebddba0c111..4d669ab4c76a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx @@ -30,7 +30,7 @@ describe('actionTypeRegistry.get() works', () => { }); describe('connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { + test('connector validation succeeds when connector config is valid', async () => { const actionConnector = { secrets: { user: 'user', @@ -49,7 +49,7 @@ describe('connector validation', () => { }, } as EmailActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { from: [], @@ -66,7 +66,7 @@ describe('connector validation', () => { }); }); - test('connector validation succeeds when connector config is valid with empty user/password', () => { + test('connector validation succeeds when connector config is valid with empty user/password', async () => { const actionConnector = { secrets: { user: null, @@ -85,7 +85,7 @@ describe('connector validation', () => { }, } as EmailActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { from: [], @@ -101,7 +101,7 @@ describe('connector validation', () => { }, }); }); - test('connector validation fails when connector config is not valid', () => { + test('connector validation fails when connector config is not valid', async () => { const actionConnector = { secrets: { user: 'user', @@ -116,7 +116,7 @@ describe('connector validation', () => { }, } as EmailActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { from: [], @@ -132,7 +132,7 @@ describe('connector validation', () => { }, }); }); - test('connector validation fails when user specified but not password', () => { + test('connector validation fails when user specified but not password', async () => { const actionConnector = { secrets: { user: 'user', @@ -151,7 +151,7 @@ describe('connector validation', () => { }, } as EmailActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { from: [], @@ -167,7 +167,7 @@ describe('connector validation', () => { }, }); }); - test('connector validation fails when password specified but not user', () => { + test('connector validation fails when password specified but not user', async () => { const actionConnector = { secrets: { user: null, @@ -186,7 +186,7 @@ describe('connector validation', () => { }, } as EmailActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { from: [], @@ -205,7 +205,7 @@ describe('connector validation', () => { }); describe('action params validation', () => { - test('action params validation succeeds when action params is valid', () => { + test('action params validation succeeds when action params is valid', async () => { const actionParams = { to: [], cc: ['test1@test.com'], @@ -213,7 +213,7 @@ describe('action params validation', () => { subject: 'test', }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { to: [], cc: [], @@ -224,13 +224,13 @@ describe('action params validation', () => { }); }); - test('action params validation fails when action params is not valid', () => { + test('action params validation fails when action params is not valid', async () => { const actionParams = { to: ['test@test.com'], subject: 'test', }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { to: [], cc: [], 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 index 81eadda4fc27..5e2375462143 100644 --- 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 @@ -31,9 +31,12 @@ export function getActionType(): ActionTypeModel, EmailSecrets> => { + ): Promise< + ConnectorValidationResult, EmailSecrets> + > => { + const translations = await import('./translations'); const configErrors = { from: new Array(), port: new Array(), @@ -49,74 +52,25 @@ export function getActionType(): ActionTypeModel => { + ): Promise> => { + const translations = await import('./translations'); const errors = { to: new Array(), cc: new Array(), @@ -146,35 +101,16 @@ export function getActionType(): ActionTypeModel 0; + const isHostInvalid: boolean = + host !== undefined && errors.host !== undefined && errors.host.length > 0; + const isPortInvalid: boolean = + port !== undefined && errors.port !== undefined && errors.port.length > 0; + + const isPasswordInvalid: boolean = + password !== undefined && errors.password !== undefined && errors.password.length > 0; + const isUserInvalid: boolean = + user !== undefined && errors.user !== undefined && errors.user.length > 0; return ( <> @@ -46,7 +57,7 @@ export const EmailActionConnectorFields: React.FunctionComponent< id="from" fullWidth error={errors.from} - isInvalid={errors.from.length > 0 && from !== undefined} + isInvalid={isFromInvalid} label={i18n.translate( 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.fromTextFieldLabel', { @@ -65,7 +76,7 @@ export const EmailActionConnectorFields: React.FunctionComponent< 0 && from !== undefined} + isInvalid={isFromInvalid} name="from" value={from || ''} data-test-subj="emailFromInput" @@ -87,7 +98,7 @@ export const EmailActionConnectorFields: React.FunctionComponent< id="emailHost" fullWidth error={errors.host} - isInvalid={errors.host.length > 0 && host !== undefined} + isInvalid={isHostInvalid} label={i18n.translate( 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hostTextFieldLabel', { @@ -98,7 +109,7 @@ export const EmailActionConnectorFields: React.FunctionComponent< 0 && host !== undefined} + isInvalid={isHostInvalid} name="host" value={host || ''} data-test-subj="emailHostInput" @@ -121,7 +132,7 @@ export const EmailActionConnectorFields: React.FunctionComponent< fullWidth placeholder="587" error={errors.port} - isInvalid={errors.port.length > 0 && port !== undefined} + isInvalid={isPortInvalid} label={i18n.translate( 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.portTextFieldLabel', { @@ -131,7 +142,7 @@ export const EmailActionConnectorFields: React.FunctionComponent< > 0 && port !== undefined} + isInvalid={isPortInvalid} fullWidth readOnly={readOnly} name="port" @@ -221,7 +232,7 @@ export const EmailActionConnectorFields: React.FunctionComponent< id="emailUser" fullWidth error={errors.user} - isInvalid={errors.user.length > 0 && user !== undefined} + isInvalid={isUserInvalid} label={i18n.translate( 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel', { @@ -231,7 +242,7 @@ export const EmailActionConnectorFields: React.FunctionComponent< > 0 && user !== undefined} + isInvalid={isUserInvalid} name="user" readOnly={readOnly} value={user || ''} @@ -252,7 +263,7 @@ export const EmailActionConnectorFields: React.FunctionComponent< id="emailPassword" fullWidth error={errors.password} - isInvalid={errors.password.length > 0 && password !== undefined} + isInvalid={isPasswordInvalid} label={i18n.translate( 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.passwordFieldLabel', { @@ -263,7 +274,7 @@ export const EmailActionConnectorFields: React.FunctionComponent< 0 && password !== undefined} + isInvalid={isPasswordInvalid} name="password" value={password || ''} data-test-subj="emailPasswordInput" diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx index e2d6237af85d..5d19a1958c1c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx @@ -44,13 +44,18 @@ export const EmailParamsFields = ({ } // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultMessage]); - + const isToInvalid: boolean = to !== undefined && errors.to !== undefined && errors.to.length > 0; + const isSubjectInvalid: boolean = + subject !== undefined && errors.subject !== undefined && errors.subject.length > 0; + const isCCInvalid: boolean = errors.cc !== undefined && errors.cc.length > 0 && cc !== undefined; + const isBCCInvalid: boolean = + errors.bcc !== undefined && errors.bcc.length > 0 && bcc !== undefined; return ( <> 0 && to !== undefined} + isInvalid={isToInvalid} label={i18n.translate( 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientTextFieldLabel', { @@ -82,7 +87,7 @@ export const EmailParamsFields = ({ > 0 && to !== undefined} + isInvalid={isToInvalid} fullWidth data-test-subj="toEmailAddressInput" selectedOptions={toOptions} @@ -112,7 +117,7 @@ export const EmailParamsFields = ({ 0 && cc !== undefined} + isInvalid={isCCInvalid} label={i18n.translate( 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientCopyTextFieldLabel', { @@ -122,7 +127,7 @@ export const EmailParamsFields = ({ > 0 && cc !== undefined} + isInvalid={isCCInvalid} fullWidth data-test-subj="ccEmailAddressInput" selectedOptions={ccOptions} @@ -153,7 +158,7 @@ export const EmailParamsFields = ({ 0 && bcc !== undefined} + isInvalid={isBCCInvalid} label={i18n.translate( 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.recipientBccTextFieldLabel', { @@ -163,7 +168,7 @@ export const EmailParamsFields = ({ > 0 && bcc !== undefined} + isInvalid={isBCCInvalid} fullWidth data-test-subj="bccEmailAddressInput" selectedOptions={bccOptions} @@ -193,7 +198,7 @@ export const EmailParamsFields = ({ 0 && subject !== undefined} + isInvalid={isSubjectInvalid} label={i18n.translate( 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.subjectTextFieldLabel', { @@ -207,7 +212,7 @@ export const EmailParamsFields = ({ messageVariables={messageVariables} paramsProperty={'subject'} inputTargetValue={subject} - errors={errors.subject as string[]} + errors={(errors.subject ?? []) as string[]} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts new file mode 100644 index 000000000000..5da9145ecec0 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const SENDER_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText', + { + defaultMessage: 'Sender is required.', + } +); + +export const SENDER_NOT_VALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText', + { + defaultMessage: 'Sender is not a valid email address.', + } +); + +export const PORT_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText', + { + defaultMessage: 'Port is required.', + } +); + +export const HOST_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredHostText', + { + defaultMessage: 'Host is required.', + } +); + +export const USERNAME_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthUserNameText', + { + defaultMessage: 'Username is required.', + } +); + +export const PASSWORD_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthPasswordText', + { + defaultMessage: 'Password is required.', + } +); + +export const PASSWORD_REQUIRED_FOR_USER_USED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPasswordText', + { + defaultMessage: 'Password is required when username is used.', + } +); + +export const TO_CC_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText', + { + defaultMessage: 'No To, Cc, or Bcc entry. At least one entry is required.', + } +); + +export const MESSAGE_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredMessageText', + { + defaultMessage: 'Message is required.', + } +); + +export const SUBJECT_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSubjectText', + { + defaultMessage: 'Subject is required.', + } +); 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 index 975765304317..f43d883be7ad 100644 --- 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 @@ -30,7 +30,7 @@ describe('actionTypeRegistry.get() works', () => { }); describe('index connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { + test('connector validation succeeds when connector config is valid', async () => { const actionConnector = { secrets: {}, id: 'test', @@ -43,7 +43,7 @@ describe('index connector validation', () => { }, } as EsIndexActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { index: [], @@ -57,7 +57,7 @@ describe('index connector validation', () => { }); describe('index connector validation with minimal config', () => { - test('connector validation succeeds when connector config is valid', () => { + test('connector validation succeeds when connector config is valid', async () => { const actionConnector = { secrets: {}, id: 'test', @@ -68,7 +68,7 @@ describe('index connector validation with minimal config', () => { }, } as EsIndexActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { index: [], @@ -82,9 +82,9 @@ describe('index connector validation with minimal config', () => { }); describe('action params validation', () => { - test('action params validation succeeds when action params are valid', () => { + test('action params validation succeeds when action params are valid', async () => { expect( - actionTypeModel.validateParams({ + await actionTypeModel.validateParams({ documents: [{ test: 1234 }], }) ).toEqual({ @@ -95,7 +95,7 @@ describe('action params validation', () => { }); expect( - actionTypeModel.validateParams({ + await actionTypeModel.validateParams({ documents: [{ test: 1234 }], indexOverride: 'kibana-alert-history-anything', }) @@ -107,8 +107,8 @@ describe('action params validation', () => { }); }); - test('action params validation fails when action params are invalid', () => { - expect(actionTypeModel.validateParams({})).toEqual({ + test('action params validation fails when action params are invalid', async () => { + expect(await actionTypeModel.validateParams({})).toEqual({ errors: { documents: ['Document is required and should be a valid JSON object.'], indexOverride: [], @@ -116,7 +116,7 @@ describe('action params validation', () => { }); expect( - actionTypeModel.validateParams({ + await actionTypeModel.validateParams({ documents: [{}], }) ).toEqual({ @@ -127,7 +127,7 @@ describe('action params validation', () => { }); expect( - actionTypeModel.validateParams({ + await actionTypeModel.validateParams({ documents: [{}], indexOverride: 'kibana-alert-history-', }) @@ -139,7 +139,7 @@ describe('action params validation', () => { }); expect( - actionTypeModel.validateParams({ + await actionTypeModel.validateParams({ documents: [{}], indexOverride: 'this.is-a_string', }) 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 index f4b8284c8cfa..80d38bda22ab 100644 --- 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 @@ -31,44 +31,32 @@ export function getActionType(): ActionTypeModel, unknown> => { + ): Promise, unknown>> => { + const translations = await import('./translations'); const configErrors = { index: new Array(), }; const validationResult = { config: { errors: configErrors }, secrets: { errors: {} } }; if (!action.config.index) { - configErrors.index.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.requiredIndexText', - { - defaultMessage: 'Index is required.', - } - ) - ); + configErrors.index.push(translations.INDEX_REQUIRED); } return validationResult; }, actionConnectorFields: lazy(() => import('./es_index_connector')), actionParamsFields: lazy(() => import('./es_index_params')), - validateParams: ( + validateParams: async ( actionParams: IndexActionParams - ): GenericValidationResult => { + ): Promise> => { + const translations = await import('./translations'); const errors = { documents: new Array(), indexOverride: new Array(), }; const validationResult = { errors }; if (!actionParams.documents?.length || Object.keys(actionParams.documents[0]).length === 0) { - errors.documents.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredDocumentJson', - { - defaultMessage: 'Document is required and should be a valid JSON object.', - } - ) - ); + errors.documents.push(translations.DOCUMENT_NOT_VALID); } if (actionParams.indexOverride) { if (!actionParams.indexOverride.startsWith(ALERT_HISTORY_PREFIX)) { @@ -85,14 +73,7 @@ export function getActionType(): ActionTypeModel 0 && index !== undefined; return ( <> @@ -95,7 +97,7 @@ const IndexActionConnectorFields: React.FunctionComponent< defaultMessage="Index" /> } - isInvalid={errors.index.length > 0 && index !== undefined} + isInvalid={isIndexInvalid} error={errors.index} helpText={ <> @@ -118,7 +120,7 @@ const IndexActionConnectorFields: React.FunctionComponent< singleSelection={{ asPlainText: true }} async isLoading={isIndiciesLoading} - isInvalid={errors.index.length > 0 && index !== undefined} + isInvalid={isIndexInvalid} noSuggestions={!indexOptions.length} options={indexOptions} data-test-subj="connectorIndexesComboBox" 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 index 6973cdcc7a08..b5985cf724e0 100644 --- 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 @@ -117,7 +117,11 @@ export const IndexParamsFields = ({ 0} + isInvalid={ + errors.indexOverride !== undefined && + (errors.indexOverride as string[]) && + errors.indexOverride.length > 0 + } label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.preconfiguredIndex', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/translations.ts new file mode 100644 index 000000000000..b7dd6ac74990 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/translations.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const INDEX_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.requiredIndexText', + { + defaultMessage: 'Index is required.', + } +); + +export const DOCUMENT_NOT_VALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredDocumentJson', + { + defaultMessage: 'Document is required and should be a valid JSON object.', + } +); + +export const HISTORY_NOT_VALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.badIndexOverrideSuffix', + { + defaultMessage: 'Alert history index must contain valid suffix.', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx index ea1bcf82c314..857582fa7cda 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx @@ -29,7 +29,7 @@ describe('actionTypeRegistry.get() works', () => { }); describe('jira connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { + test('connector validation succeeds when connector config is valid', async () => { const actionConnector = { secrets: { email: 'email', @@ -45,7 +45,7 @@ describe('jira connector validation', () => { }, } as JiraActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { apiUrl: [], @@ -61,7 +61,7 @@ describe('jira connector validation', () => { }); }); - test('connector validation fails when connector config is not valid', () => { + test('connector validation fails when connector config is not valid', async () => { const actionConnector = ({ secrets: { email: 'user', @@ -72,7 +72,7 @@ describe('jira connector validation', () => { config: {}, } as unknown) as JiraActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { apiUrl: ['URL is required.'], @@ -90,22 +90,22 @@ describe('jira connector validation', () => { }); describe('jira action params validation', () => { - test('action params validation succeeds when action params is valid', () => { + test('action params validation succeeds when action params is valid', async () => { const actionParams = { subActionParams: { incident: { summary: 'some title {{test}}' }, comments: [] }, }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { 'subActionParams.incident.summary': [], 'subActionParams.incident.labels': [] }, }); }); - test('params validation fails when body is not valid', () => { + test('params validation fails when body is not valid', async () => { const actionParams = { subActionParams: { incident: { summary: '' }, comments: [] }, }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { 'subActionParams.incident.summary': ['Summary is required.'], 'subActionParams.incident.labels': [], @@ -113,7 +113,7 @@ describe('jira action params validation', () => { }); }); - test('params validation fails when labels contain spaces', () => { + test('params validation fails when labels contain spaces', async () => { const actionParams = { subActionParams: { incident: { summary: 'some title', labels: ['label with spaces'] }, @@ -121,7 +121,7 @@ describe('jira action params validation', () => { }, }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { 'subActionParams.incident.summary': [], 'subActionParams.incident.labels': ['Labels cannot contain spaces.'], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx index ff7fd026f8e3..8e3424a16c29 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx @@ -6,18 +6,19 @@ */ import { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; import { GenericValidationResult, ActionTypeModel, ConnectorValidationResult, } from '../../../../types'; import { JiraActionConnector, JiraConfig, JiraSecrets, JiraActionParams } from './types'; -import * as i18n from './translations'; import { isValidUrl } from '../../../lib/value_validators'; -const validateConnector = ( +const validateConnector = async ( action: JiraActionConnector -): ConnectorValidationResult => { +): Promise> => { + const translations = await import('./translations'); const configErrors = { apiUrl: new Array(), projectKey: new Array(), @@ -33,41 +34,58 @@ const validateConnector = ( }; if (!action.config.apiUrl) { - configErrors.apiUrl = [...configErrors.apiUrl, i18n.API_URL_REQUIRED]; + configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRED]; } if (action.config.apiUrl) { if (!isValidUrl(action.config.apiUrl)) { - configErrors.apiUrl = [...configErrors.apiUrl, i18n.API_URL_INVALID]; + configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_INVALID]; } else if (!isValidUrl(action.config.apiUrl, 'https:')) { - configErrors.apiUrl = [...configErrors.apiUrl, i18n.API_URL_REQUIRE_HTTPS]; + configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRE_HTTPS]; } } if (!action.config.projectKey) { - configErrors.projectKey = [...configErrors.projectKey, i18n.JIRA_PROJECT_KEY_REQUIRED]; + configErrors.projectKey = [...configErrors.projectKey, translations.JIRA_PROJECT_KEY_REQUIRED]; } if (!action.secrets.email) { - secretsErrors.email = [...secretsErrors.email, i18n.JIRA_EMAIL_REQUIRED]; + secretsErrors.email = [...secretsErrors.email, translations.JIRA_EMAIL_REQUIRED]; } if (!action.secrets.apiToken) { - secretsErrors.apiToken = [...secretsErrors.apiToken, i18n.JIRA_API_TOKEN_REQUIRED]; + secretsErrors.apiToken = [...secretsErrors.apiToken, translations.JIRA_API_TOKEN_REQUIRED]; } return validationResult; }; +export const JIRA_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.selectMessageText', + { + defaultMessage: 'Create an incident in Jira.', + } +); + +export const JIRA_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.actionTypeTitle', + { + defaultMessage: 'Jira', + } +); + export function getActionType(): ActionTypeModel { return { id: '.jira', iconClass: lazy(() => import('./logo')), - selectMessage: i18n.JIRA_DESC, - actionTypeTitle: i18n.JIRA_TITLE, + selectMessage: JIRA_DESC, + actionTypeTitle: JIRA_TITLE, validateConnector, actionConnectorFields: lazy(() => import('./jira_connectors')), - validateParams: (actionParams: JiraActionParams): GenericValidationResult => { + validateParams: async ( + actionParams: JiraActionParams + ): Promise> => { + const translations = await import('./translations'); const errors = { 'subActionParams.incident.summary': new Array(), 'subActionParams.incident.labels': new Array(), @@ -80,13 +98,13 @@ export function getActionType(): ActionTypeModel label.match(/\s/g))) - errors['subActionParams.incident.labels'].push(i18n.LABELS_WHITE_SPACES); + errors['subActionParams.incident.labels'].push(translations.LABELS_WHITE_SPACES); } return validationResult; }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx index f2753310d73a..7aec0a405d0d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx @@ -32,13 +32,17 @@ const JiraConnectorFields: React.FC { const { apiUrl, projectKey } = action.config; - const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl !== undefined; + const isApiUrlInvalid: boolean = + apiUrl !== undefined && errors.apiUrl !== undefined && errors.apiUrl.length > 0; const { email, apiToken } = action.secrets; - const isProjectKeyInvalid: boolean = errors.projectKey.length > 0 && projectKey !== undefined; - const isEmailInvalid: boolean = errors.email.length > 0 && email !== undefined; - const isApiTokenInvalid: boolean = errors.apiToken.length > 0 && apiToken !== undefined; + const isProjectKeyInvalid: boolean = + projectKey !== undefined && errors.projectKey !== undefined && errors.projectKey.length > 0; + const isEmailInvalid: boolean = + email !== undefined && errors.email !== undefined && errors.email.length > 0; + const isApiTokenInvalid: boolean = + apiToken !== undefined && errors.apiToken !== undefined && errors.apiToken.length > 0; const handleOnChangeActionConfig = useCallback( (key: string, value: string) => editActionConfig(key, value), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index 11123a81440b..5897de46f94d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -186,6 +186,7 @@ const JiraParamsFields: React.FunctionComponent 0 && incident.labels !== undefined; @@ -277,6 +278,7 @@ const JiraParamsFields: React.FunctionComponent 0 && incident.summary !== undefined } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts index 4577e55260d9..5904eb05c31b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -7,20 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const JIRA_DESC = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.selectMessageText', - { - defaultMessage: 'Create an incident in Jira.', - } -); - -export const JIRA_TITLE = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.actionTypeTitle', - { - defaultMessage: 'Jira', - } -); - export const API_URL_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.apiUrlTextFieldLabel', { 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 index eae8690dbdd9..d96ca76aea3b 100644 --- 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 @@ -30,7 +30,7 @@ describe('actionTypeRegistry.get() works', () => { }); describe('pagerduty connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { + test('connector validation succeeds when connector config is valid', async () => { const actionConnector = { secrets: { routingKey: 'test', @@ -43,7 +43,7 @@ describe('pagerduty connector validation', () => { }, } as PagerDutyActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ secrets: { errors: { routingKey: [], @@ -53,7 +53,7 @@ describe('pagerduty connector validation', () => { delete actionConnector.config.apiUrl; actionConnector.secrets.routingKey = 'test1'; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ secrets: { errors: { routingKey: [], @@ -62,7 +62,7 @@ describe('pagerduty connector validation', () => { }); }); - test('connector validation fails when connector config is not valid', () => { + test('connector validation fails when connector config is not valid', async () => { const actionConnector = { secrets: {}, id: 'test', @@ -73,7 +73,7 @@ describe('pagerduty connector validation', () => { }, } as PagerDutyActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ secrets: { errors: { routingKey: ['An integration key / routing key is required.'], @@ -84,7 +84,7 @@ describe('pagerduty connector validation', () => { }); describe('pagerduty action params validation', () => { - test('action params validation succeeds when action params is valid', () => { + test('action params validation succeeds when action params is valid', async () => { const actionParams = { eventAction: 'trigger', dedupKey: 'test', @@ -97,7 +97,7 @@ describe('pagerduty action params validation', () => { class: 'test class', }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { dedupKey: [], summary: [], 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 index 310c5cae2456..80dd360d620b 100644 --- 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 @@ -42,9 +42,10 @@ export function getActionType(): ActionTypeModel< defaultMessage: 'Send to PagerDuty', } ), - validateConnector: ( + validateConnector: async ( action: PagerDutyActionConnector - ): ConnectorValidationResult => { + ): Promise> => { + const translations = await import('./translations'); const secretsErrors = { routingKey: new Array(), }; @@ -53,22 +54,16 @@ export function getActionType(): ActionTypeModel< }; if (!action.secrets.routingKey) { - secretsErrors.routingKey.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredRoutingKeyText', - { - defaultMessage: 'An integration key / routing key is required.', - } - ) - ); + secretsErrors.routingKey.push(translations.INTEGRATION_KEY_REQUIRED); } return validationResult; }, - validateParams: ( + validateParams: async ( actionParams: PagerDutyActionParams - ): GenericValidationResult< - Pick + ): Promise< + GenericValidationResult> > => { + const translations = await import('./translations'); const errors = { summary: new Array(), timestamp: new Array(), @@ -79,27 +74,13 @@ export function getActionType(): ActionTypeModel< !actionParams.dedupKey?.length && (actionParams.eventAction === 'resolve' || actionParams.eventAction === 'acknowledge') ) { - errors.dedupKey.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredDedupKeyText', - { - defaultMessage: 'DedupKey is required when resolving or acknowledging an incident.', - } - ) - ); + errors.dedupKey.push(translations.DEDUP_KEY_REQUIRED); } if ( actionParams.eventAction === EventActionOptions.TRIGGER && !actionParams.summary?.length ) { - errors.summary.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredSummaryText', - { - defaultMessage: 'Summary is required.', - } - ) - ); + errors.summary.push(translations.SUMMARY_REQUIRED); } if (actionParams.timestamp && !hasMustacheTokens(actionParams.timestamp)) { if (isNaN(Date.parse(actionParams.timestamp))) { 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 index 7e9a5770c215..3ac7832d0462 100644 --- 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 @@ -20,6 +20,9 @@ const PagerDutyActionConnectorFields: React.FunctionComponent< const { docLinks } = useKibana().services; const { apiUrl } = action.config; const { routingKey } = action.secrets; + const isRoutingKeyInvalid: boolean = + routingKey !== undefined && errors.routingKey !== undefined && errors.routingKey.length > 0; + return ( <> } error={errors.routingKey} - isInvalid={errors.routingKey.length > 0 && routingKey !== undefined} + isInvalid={isRoutingKeyInvalid} label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyTextFieldLabel', { @@ -80,7 +83,7 @@ const PagerDutyActionConnectorFields: React.FunctionComponent< )} 0 && routingKey !== undefined} + isInvalid={isRoutingKeyInvalid} name="routingKey" readOnly={readOnly} value={routingKey || ''} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx index 4961a27fd0ac..8605832b92ea 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx @@ -101,6 +101,12 @@ const PagerDutyParamsFields: React.FunctionComponent 0; + const isSummaryInvalid: boolean = + errors.summary !== undefined && errors.summary.length > 0 && summary !== undefined; + const isTimestampInvalid: boolean = + errors.timestamp !== undefined && errors.timestamp.length > 0 && timestamp !== undefined; + return ( <> @@ -132,7 +138,7 @@ const PagerDutyParamsFields: React.FunctionComponent 0} + isInvalid={isDedupKeyInvalid} label={ isDedupeKeyRequired ? i18n.translate( @@ -166,7 +172,7 @@ const PagerDutyParamsFields: React.FunctionComponent 0 && summary !== undefined} + isInvalid={isSummaryInvalid} label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.summaryFieldLabel', { @@ -180,7 +186,7 @@ const PagerDutyParamsFields: React.FunctionComponent @@ -211,7 +217,7 @@ const PagerDutyParamsFields: React.FunctionComponent 0 && timestamp !== undefined} + isInvalid={isTimestampInvalid} label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.timestampTextFieldLabel', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/translations.ts new file mode 100644 index 000000000000..a907b19a1d73 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/translations.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const SUMMARY_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredSummaryText', + { + defaultMessage: 'Summary is required.', + } +); + +export const DEDUP_KEY_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredDedupKeyText', + { + defaultMessage: 'DedupKey is required when resolving or acknowledging an incident.', + } +); + +export const INTEGRATION_KEY_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredRoutingKeyText', + { + defaultMessage: 'An integration key / routing key is required.', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx index 892ab97b8627..93fb419f509b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx @@ -29,7 +29,7 @@ describe('actionTypeRegistry.get() works', () => { }); describe('resilient connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { + test('connector validation succeeds when connector config is valid', async () => { const actionConnector = { secrets: { apiKeyId: 'email', @@ -45,7 +45,7 @@ describe('resilient connector validation', () => { }, } as ResilientActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { apiUrl: [], @@ -61,7 +61,7 @@ describe('resilient connector validation', () => { }); }); - test('connector validation fails when connector config is not valid', () => { + test('connector validation fails when connector config is not valid', async () => { const actionConnector = ({ secrets: { apiKeyId: 'user', @@ -72,7 +72,7 @@ describe('resilient connector validation', () => { config: {}, } as unknown) as ResilientActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { apiUrl: ['URL is required.'], @@ -90,22 +90,22 @@ describe('resilient connector validation', () => { }); describe('resilient action params validation', () => { - test('action params validation succeeds when action params is valid', () => { + test('action params validation succeeds when action params is valid', async () => { const actionParams = { subActionParams: { incident: { name: 'some title {{test}}' }, comments: [] }, }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { 'subActionParams.incident.name': [] }, }); }); - test('params validation fails when body is not valid', () => { + test('params validation fails when body is not valid', async () => { const actionParams = { subActionParams: { incident: { name: '' }, comments: [] }, }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { 'subActionParams.incident.name': ['Name is required.'], }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx index e7074b7506e7..f20204af1769 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx @@ -6,6 +6,7 @@ */ import { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; import { GenericValidationResult, ActionTypeModel, @@ -17,12 +18,12 @@ import { ResilientSecrets, ResilientActionParams, } from './types'; -import * as i18n from './translations'; import { isValidUrl } from '../../../lib/value_validators'; -const validateConnector = ( +const validateConnector = async ( action: ResilientActionConnector -): ConnectorValidationResult => { +): Promise> => { + const translations = await import('./translations'); const configErrors = { apiUrl: new Array(), orgId: new Array(), @@ -38,32 +39,49 @@ const validateConnector = ( }; if (!action.config.apiUrl) { - configErrors.apiUrl = [...configErrors.apiUrl, i18n.API_URL_REQUIRED]; + configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRED]; } if (action.config.apiUrl) { if (!isValidUrl(action.config.apiUrl)) { - configErrors.apiUrl = [...configErrors.apiUrl, i18n.API_URL_INVALID]; + configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_INVALID]; } else if (!isValidUrl(action.config.apiUrl, 'https:')) { - configErrors.apiUrl = [...configErrors.apiUrl, i18n.API_URL_REQUIRE_HTTPS]; + configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRE_HTTPS]; } } if (!action.config.orgId) { - configErrors.orgId = [...configErrors.orgId, i18n.ORG_ID_REQUIRED]; + configErrors.orgId = [...configErrors.orgId, translations.ORG_ID_REQUIRED]; } if (!action.secrets.apiKeyId) { - secretsErrors.apiKeyId = [...secretsErrors.apiKeyId, i18n.API_KEY_ID_REQUIRED]; + secretsErrors.apiKeyId = [...secretsErrors.apiKeyId, translations.API_KEY_ID_REQUIRED]; } if (!action.secrets.apiKeySecret) { - secretsErrors.apiKeySecret = [...secretsErrors.apiKeySecret, i18n.API_KEY_SECRET_REQUIRED]; + secretsErrors.apiKeySecret = [ + ...secretsErrors.apiKeySecret, + translations.API_KEY_SECRET_REQUIRED, + ]; } return validationResult; }; +export const DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText', + { + defaultMessage: 'Create an incident in IBM Resilient.', + } +); + +export const TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.actionTypeTitle', + { + defaultMessage: 'Resilient', + } +); + export function getActionType(): ActionTypeModel< ResilientConfig, ResilientSecrets, @@ -72,11 +90,14 @@ export function getActionType(): ActionTypeModel< return { id: '.resilient', iconClass: lazy(() => import('./logo')), - selectMessage: i18n.DESC, - actionTypeTitle: i18n.TITLE, + selectMessage: DESC, + actionTypeTitle: TITLE, validateConnector, actionConnectorFields: lazy(() => import('./resilient_connectors')), - validateParams: (actionParams: ResilientActionParams): GenericValidationResult => { + validateParams: async ( + actionParams: ResilientActionParams + ): Promise> => { + const translations = await import('./translations'); const errors = { 'subActionParams.incident.name': new Array(), }; @@ -88,7 +109,7 @@ export function getActionType(): ActionTypeModel< actionParams.subActionParams.incident && !actionParams.subActionParams.incident.name?.length ) { - errors['subActionParams.incident.name'].push(i18n.NAME_REQUIRED); + errors['subActionParams.incident.name'].push(translations.NAME_REQUIRED); } return validationResult; }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx index 6996062899c3..1270f19820f4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx @@ -30,14 +30,19 @@ const ResilientConnectorFields: React.FC { const { apiUrl, orgId } = action.config; - const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl !== undefined; + const isApiUrlInvalid: boolean = + apiUrl !== undefined && errors.apiUrl !== undefined && errors.apiUrl.length > 0; const { apiKeyId, apiKeySecret } = action.secrets; - const isOrgIdInvalid: boolean = errors.orgId.length > 0 && orgId !== undefined; - const isApiKeyInvalid: boolean = errors.apiKeyId.length > 0 && apiKeyId !== undefined; + const isOrgIdInvalid: boolean = + orgId !== undefined && errors.orgId !== undefined && errors.orgId.length > 0; + const isApiKeyInvalid: boolean = + apiKeyId !== undefined && errors.apiKeyId !== undefined && errors.apiKeyId.length > 0; const isApiKeySecretInvalid: boolean = - errors.apiKeySecret.length > 0 && apiKeySecret !== undefined; + apiKeySecret !== undefined && + errors.apiKeySecret !== undefined && + errors.apiKeySecret.length > 0; const handleOnChangeActionConfig = useCallback( (key: string, value: string) => editActionConfig(key, value), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx index 4642226d4022..54a138a2bc7c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx @@ -213,7 +213,9 @@ const ResilientParamsFields: React.FunctionComponent 0 && incident.name !== undefined + errors['subActionParams.incident.name'] !== undefined && + errors['subActionParams.incident.name'].length > 0 && + incident.name !== undefined } label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.nameFieldLabel', @@ -226,7 +228,7 @@ const ResilientParamsFields: React.FunctionComponent { }); describe('server-log connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { + test('connector validation succeeds when connector config is valid', async () => { const actionConnector: UserConfiguredActionConnector<{}, {}> = { secrets: {}, id: 'test', @@ -39,7 +39,7 @@ describe('server-log connector validation', () => { isPreconfigured: false, }; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: {}, }, @@ -51,23 +51,23 @@ describe('server-log connector validation', () => { }); describe('action params validation', () => { - test('action params validation succeeds when action params is valid', () => { + test('action params validation succeeds when action params is valid', async () => { const actionParams = { message: 'test message', level: 'trace', }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { message: [] }, }); }); - test('params validation fails when message is not valid', () => { + test('params validation fails when message is not valid', async () => { const actionParams = { message: '', }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await 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 index 4550d2d65b9d..066c5c0a2f38 100644 --- 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 @@ -30,12 +30,12 @@ export function getActionType(): ActionTypeModel => { - return { config: { errors: {} }, secrets: { errors: {} } }; + validateConnector: (): Promise> => { + return Promise.resolve({ config: { errors: {} }, secrets: { errors: {} } }); }, validateParams: ( actionParams: ServerLogActionParams - ): GenericValidationResult> => { + ): Promise>> => { const errors = { message: new Array(), }; @@ -50,7 +50,7 @@ export function getActionType(): ActionTypeModel import('./server_log_params')), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx index 02ecab47ae49..e25e8120b165 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx @@ -30,7 +30,7 @@ describe('actionTypeRegistry.get() works', () => { describe('servicenow connector validation', () => { [SERVICENOW_ITSM_ACTION_TYPE_ID, SERVICENOW_SIR_ACTION_TYPE_ID].forEach((id) => { - test(`${id}: connector validation succeeds when connector config is valid`, () => { + test(`${id}: connector validation succeeds when connector config is valid`, async () => { const actionTypeModel = actionTypeRegistry.get(id); const actionConnector = { secrets: { @@ -46,7 +46,7 @@ describe('servicenow connector validation', () => { }, } as ServiceNowActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { apiUrl: [], @@ -61,7 +61,7 @@ describe('servicenow connector validation', () => { }); }); - test(`${id}: connector validation fails when connector config is not valid`, () => { + test(`${id}: connector validation fails when connector config is not valid`, async () => { const actionTypeModel = actionTypeRegistry.get(id); const actionConnector = ({ secrets: { @@ -73,7 +73,7 @@ describe('servicenow connector validation', () => { config: {}, } as unknown) as ServiceNowActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { apiUrl: ['URL is required.'], @@ -92,24 +92,24 @@ describe('servicenow connector validation', () => { describe('servicenow action params validation', () => { [SERVICENOW_ITSM_ACTION_TYPE_ID, SERVICENOW_SIR_ACTION_TYPE_ID].forEach((id) => { - test(`${id}: action params validation succeeds when action params is valid`, () => { + test(`${id}: action params validation succeeds when action params is valid`, async () => { const actionTypeModel = actionTypeRegistry.get(id); const actionParams = { subActionParams: { incident: { short_description: 'some title {{test}}' }, comments: [] }, }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { ['subActionParams.incident.short_description']: [] }, }); }); - test(`${id}: params validation fails when body is not valid`, () => { + test(`${id}: params validation fails when body is not valid`, async () => { const actionTypeModel = actionTypeRegistry.get(id); const actionParams = { subActionParams: { incident: { short_description: '' }, comments: [] }, }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { ['subActionParams.incident.short_description']: ['Short description is required.'], }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx index a6cc116d3d7b..24e2a87d4235 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx @@ -6,6 +6,7 @@ */ import { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; import { GenericValidationResult, ActionTypeModel, @@ -18,12 +19,12 @@ import { ServiceNowITSMActionParams, ServiceNowSIRActionParams, } from './types'; -import * as i18n from './translations'; import { isValidUrl } from '../../../lib/value_validators'; -const validateConnector = ( +const validateConnector = async ( action: ServiceNowActionConnector -): ConnectorValidationResult => { +): Promise> => { + const translations = await import('./translations'); const configErrors = { apiUrl: new Array(), }; @@ -38,28 +39,56 @@ const validateConnector = ( }; if (!action.config.apiUrl) { - configErrors.apiUrl = [...configErrors.apiUrl, i18n.API_URL_REQUIRED]; + configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRED]; } if (action.config.apiUrl) { if (!isValidUrl(action.config.apiUrl)) { - configErrors.apiUrl = [...configErrors.apiUrl, i18n.API_URL_INVALID]; + configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_INVALID]; } else if (!isValidUrl(action.config.apiUrl, 'https:')) { - configErrors.apiUrl = [...configErrors.apiUrl, i18n.API_URL_REQUIRE_HTTPS]; + configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRE_HTTPS]; } } if (!action.secrets.username) { - secretsErrors.username = [...secretsErrors.username, i18n.USERNAME_REQUIRED]; + secretsErrors.username = [...secretsErrors.username, translations.USERNAME_REQUIRED]; } if (!action.secrets.password) { - secretsErrors.password = [...secretsErrors.password, i18n.PASSWORD_REQUIRED]; + secretsErrors.password = [...secretsErrors.password, translations.PASSWORD_REQUIRED]; } return validationResult; }; +export const SERVICENOW_ITSM_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowITSM.selectMessageText', + { + defaultMessage: 'Create an incident in ServiceNow ITSM.', + } +); + +export const SERVICENOW_SIR_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIR.selectMessageText', + { + defaultMessage: 'Create an incident in ServiceNow SecOps.', + } +); + +export const SERVICENOW_ITSM_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowITSM.actionTypeTitle', + { + defaultMessage: 'ServiceNow ITSM', + } +); + +export const SERVICENOW_SIR_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIR.actionTypeTitle', + { + defaultMessage: 'ServiceNow SecOps', + } +); + export function getServiceNowITSMActionType(): ActionTypeModel< ServiceNowConfig, ServiceNowSecrets, @@ -68,13 +97,14 @@ export function getServiceNowITSMActionType(): ActionTypeModel< return { id: '.servicenow', iconClass: lazy(() => import('./logo')), - selectMessage: i18n.SERVICENOW_ITSM_DESC, - actionTypeTitle: i18n.SERVICENOW_ITSM_TITLE, + selectMessage: SERVICENOW_ITSM_DESC, + actionTypeTitle: SERVICENOW_ITSM_TITLE, validateConnector, actionConnectorFields: lazy(() => import('./servicenow_connectors')), - validateParams: ( + validateParams: async ( actionParams: ServiceNowITSMActionParams - ): GenericValidationResult => { + ): Promise> => { + const translations = await import('./translations'); const errors = { // eslint-disable-next-line @typescript-eslint/naming-convention 'subActionParams.incident.short_description': new Array(), @@ -87,7 +117,7 @@ export function getServiceNowITSMActionType(): ActionTypeModel< actionParams.subActionParams.incident && !actionParams.subActionParams.incident.short_description?.length ) { - errors['subActionParams.incident.short_description'].push(i18n.TITLE_REQUIRED); + errors['subActionParams.incident.short_description'].push(translations.TITLE_REQUIRED); } return validationResult; }, @@ -103,11 +133,14 @@ export function getServiceNowSIRActionType(): ActionTypeModel< return { id: '.servicenow-sir', iconClass: lazy(() => import('./logo')), - selectMessage: i18n.SERVICENOW_SIR_DESC, - actionTypeTitle: i18n.SERVICENOW_SIR_TITLE, + selectMessage: SERVICENOW_SIR_DESC, + actionTypeTitle: SERVICENOW_SIR_TITLE, validateConnector, actionConnectorFields: lazy(() => import('./servicenow_connectors')), - validateParams: (actionParams: ServiceNowSIRActionParams): GenericValidationResult => { + validateParams: async ( + actionParams: ServiceNowSIRActionParams + ): Promise> => { + const translations = await import('./translations'); const errors = { // eslint-disable-next-line @typescript-eslint/naming-convention 'subActionParams.incident.short_description': new Array(), @@ -120,7 +153,7 @@ export function getServiceNowSIRActionType(): ActionTypeModel< actionParams.subActionParams.incident && !actionParams.subActionParams.incident.short_description?.length ) { - errors['subActionParams.incident.short_description'].push(i18n.TITLE_REQUIRED); + errors['subActionParams.incident.short_description'].push(translations.TITLE_REQUIRED); } return validationResult; }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx index e7b2c4bac591..c9aafc58f3ed 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx @@ -32,12 +32,15 @@ const ServiceNowConnectorFields: React.FC< const { docLinks } = useKibana().services; const { apiUrl } = action.config; - const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl !== undefined; + const isApiUrlInvalid: boolean = + errors.apiUrl !== undefined && errors.apiUrl.length > 0 && apiUrl !== undefined; const { username, password } = action.secrets; - const isUsernameInvalid: boolean = errors.username.length > 0 && username !== undefined; - const isPasswordInvalid: boolean = errors.password.length > 0 && password !== undefined; + const isUsernameInvalid: boolean = + errors.username !== undefined && errors.username.length > 0 && username !== undefined; + const isPasswordInvalid: boolean = + errors.password !== undefined && errors.password.length > 0 && password !== undefined; const handleOnChangeActionConfig = useCallback( (key: string, value: string) => editActionConfig(key, value), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx index dbd6fec3dad1..f0fc5ed42d24 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx @@ -240,6 +240,7 @@ const ServiceNowParamsFields: React.FunctionComponent< fullWidth error={errors['subActionParams.incident.short_description']} isInvalid={ + errors['subActionParams.incident.short_description'] !== undefined && errors['subActionParams.incident.short_description'].length > 0 && incident.short_description !== undefined } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx index be6756b1c104..a991ee29c85f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx @@ -151,6 +151,7 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< fullWidth error={errors['subActionParams.incident.short_description']} isInvalid={ + errors['subActionParams.incident.short_description'] !== undefined && errors['subActionParams.incident.short_description'].length > 0 && incident.short_description !== undefined } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts index 288b6e629112..ea646b896f5e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts @@ -7,34 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const SERVICENOW_ITSM_DESC = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowITSM.selectMessageText', - { - defaultMessage: 'Create an incident in ServiceNow ITSM.', - } -); - -export const SERVICENOW_SIR_DESC = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIR.selectMessageText', - { - defaultMessage: 'Create an incident in ServiceNow SecOps.', - } -); - -export const SERVICENOW_ITSM_TITLE = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowITSM.actionTypeTitle', - { - defaultMessage: 'ServiceNow ITSM', - } -); - -export const SERVICENOW_SIR_TITLE = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIR.actionTypeTitle', - { - defaultMessage: 'ServiceNow SecOps', - } -); - export const API_URL_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlTextFieldLabel', { 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 index eabb63567ea8..dbdc123e0098 100644 --- 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 @@ -30,7 +30,7 @@ describe('actionTypeRegistry.get() works', () => { }); describe('slack connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { + test('connector validation succeeds when connector config is valid', async () => { const actionConnector = { secrets: { webhookUrl: 'https:\\test', @@ -41,7 +41,7 @@ describe('slack connector validation', () => { config: {}, } as SlackActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: {}, }, @@ -53,7 +53,7 @@ describe('slack connector validation', () => { }); }); - test('connector validation fails when connector config is not valid - no webhook url', () => { + test('connector validation fails when connector config is not valid - no webhook url', async () => { const actionConnector = { secrets: {}, id: 'test', @@ -62,7 +62,7 @@ describe('slack connector validation', () => { config: {}, } as SlackActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: {}, }, @@ -74,7 +74,7 @@ describe('slack connector validation', () => { }); }); - test('connector validation fails when connector config is not valid - invalid webhook protocol', () => { + test('connector validation fails when connector config is not valid - invalid webhook protocol', async () => { const actionConnector = { secrets: { webhookUrl: 'http:\\test', @@ -85,7 +85,7 @@ describe('slack connector validation', () => { config: {}, } as SlackActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: {}, }, @@ -97,7 +97,7 @@ describe('slack connector validation', () => { }); }); - test('connector validation fails when connector config is not valid - invalid webhook url', () => { + test('connector validation fails when connector config is not valid - invalid webhook url', async () => { const actionConnector = { secrets: { webhookUrl: 'h', @@ -108,7 +108,7 @@ describe('slack connector validation', () => { config: {}, } as SlackActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: {}, }, @@ -122,22 +122,22 @@ describe('slack connector validation', () => { }); describe('slack action params validation', () => { - test('if action params validation succeeds when action params is valid', () => { + test('if action params validation succeeds when action params is valid', async () => { const actionParams = { message: 'message {test}', }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { message: [] }, }); }); - test('params validation fails when message is not valid', () => { + test('params validation fails when message is not valid', async () => { const actionParams = { message: '', }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await 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 index 30e60a6ac015..d3df034a90bf 100644 --- 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 @@ -31,61 +31,35 @@ export function getActionType(): ActionTypeModel => { + ): Promise> => { + const translations = await import('./translations'); const secretsErrors = { webhookUrl: new Array(), }; const validationResult = { config: { errors: {} }, secrets: { errors: secretsErrors } }; if (!action.secrets.webhookUrl) { - secretsErrors.webhookUrl.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText', - { - defaultMessage: 'Webhook URL is required.', - } - ) - ); + secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_REQUIRED); } else if (action.secrets.webhookUrl) { if (!isValidUrl(action.secrets.webhookUrl)) { - secretsErrors.webhookUrl.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.invalidWebhookUrlText', - { - defaultMessage: 'Webhook URL is invalid.', - } - ) - ); + secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_INVALID); } else if (!isValidUrl(action.secrets.webhookUrl, 'https:')) { - secretsErrors.webhookUrl.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requireHttpsWebhookUrlText', - { - defaultMessage: 'Webhook URL must start with https://.', - } - ) - ); + secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_HTTP_INVALID); } } return validationResult; }, - validateParams: ( + validateParams: async ( actionParams: SlackActionParams - ): GenericValidationResult => { + ): Promise> => { + const translations = await import('./translations'); const errors = { message: new Array(), }; const validationResult = { errors }; if (!actionParams.message?.length) { - errors.message.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSlackMessageText', - { - defaultMessage: 'Message is required.', - } - ) - ); + errors.message.push(translations.MESSAGE_REQUIRED); } return validationResult; }, 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 index ce6cda1294ad..e87b00dca934 100644 --- 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 @@ -19,6 +19,8 @@ const SlackActionFields: React.FunctionComponent< > = ({ action, editActionSecrets, errors, readOnly }) => { const { docLinks } = useKibana().services; const { webhookUrl } = action.secrets; + const isWebhookUrlInvalid: boolean = + errors.webhookUrl !== undefined && errors.webhookUrl.length > 0 && webhookUrl !== undefined; return ( <> @@ -34,7 +36,7 @@ const SlackActionFields: React.FunctionComponent< } error={errors.webhookUrl} - isInvalid={errors.webhookUrl.length > 0 && webhookUrl !== undefined} + isInvalid={isWebhookUrlInvalid} label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel', { @@ -54,7 +56,7 @@ const SlackActionFields: React.FunctionComponent< )} 0 && webhookUrl !== undefined} + isInvalid={isWebhookUrlInvalid} name="webhookUrl" readOnly={readOnly} value={webhookUrl || ''} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.tsx index 3aa7fd822749..59e10277cfe0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.tsx @@ -49,7 +49,7 @@ const SlackParamsFields: React.FunctionComponent ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/translations.ts new file mode 100644 index 000000000000..bd1fd8ea194f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/translations.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const WEBHOOK_URL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText', + { + defaultMessage: 'Webhook URL is required.', + } +); + +export const WEBHOOK_URL_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.invalidWebhookUrlText', + { + defaultMessage: 'Webhook URL is invalid.', + } +); + +export const WEBHOOK_URL_HTTP_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requireHttpsWebhookUrlText', + { + defaultMessage: 'Webhook URL must start with https://.', + } +); + +export const MESSAGE_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSlackMessageText', + { + defaultMessage: 'Message is required.', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.test.tsx index 62be20a9bad9..641c46af6bfc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.test.tsx @@ -29,7 +29,7 @@ describe('actionTypeRegistry.get() works', () => { }); describe('teams connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { + test('connector validation succeeds when connector config is valid', async () => { const actionConnector = { secrets: { webhookUrl: 'https:\\test', @@ -40,7 +40,7 @@ describe('teams connector validation', () => { config: {}, } as TeamsActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: {}, }, @@ -52,7 +52,7 @@ describe('teams connector validation', () => { }); }); - test('connector validation fails when connector config is not valid - empty webhook url', () => { + test('connector validation fails when connector config is not valid - empty webhook url', async () => { const actionConnector = { secrets: {}, id: 'test', @@ -61,7 +61,7 @@ describe('teams connector validation', () => { config: {}, } as TeamsActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: {}, }, @@ -73,7 +73,7 @@ describe('teams connector validation', () => { }); }); - test('connector validation fails when connector config is not valid - invalid webhook url', () => { + test('connector validation fails when connector config is not valid - invalid webhook url', async () => { const actionConnector = { secrets: { webhookUrl: 'h', @@ -84,7 +84,7 @@ describe('teams connector validation', () => { config: {}, } as TeamsActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: {}, }, @@ -96,7 +96,7 @@ describe('teams connector validation', () => { }); }); - test('connector validation fails when connector config is not valid - invalid webhook url protocol', () => { + test('connector validation fails when connector config is not valid - invalid webhook url protocol', async () => { const actionConnector = { secrets: { webhookUrl: 'http://insecure', @@ -107,7 +107,7 @@ describe('teams connector validation', () => { config: {}, } as TeamsActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: {}, }, @@ -121,22 +121,22 @@ describe('teams connector validation', () => { }); describe('teams action params validation', () => { - test('if action params validation succeeds when action params is valid', () => { + test('if action params validation succeeds when action params is valid', async () => { const actionParams = { message: 'message {test}', }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { message: [] }, }); }); - test('params validation fails when message is not valid', () => { + test('params validation fails when message is not valid', async () => { const actionParams = { message: '', }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { message: ['Message is required.'], }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.tsx index e8c7be7311c1..c48b4f950855 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.tsx @@ -31,61 +31,35 @@ export function getActionType(): ActionTypeModel => { + ): Promise> => { + const translations = await import('./translations'); const secretsErrors = { webhookUrl: new Array(), }; const validationResult = { config: { errors: {} }, secrets: { errors: secretsErrors } }; if (!action.secrets.webhookUrl) { - secretsErrors.webhookUrl.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredWebhookUrlText', - { - defaultMessage: 'Webhook URL is required.', - } - ) - ); + secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_REQUIRED); } else if (action.secrets.webhookUrl) { if (!isValidUrl(action.secrets.webhookUrl)) { - secretsErrors.webhookUrl.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.invalidWebhookUrlText', - { - defaultMessage: 'Webhook URL is invalid.', - } - ) - ); + secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_INVALID); } else if (!isValidUrl(action.secrets.webhookUrl, 'https:')) { - secretsErrors.webhookUrl.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requireHttpsWebhookUrlText', - { - defaultMessage: 'Webhook URL must start with https://.', - } - ) - ); + secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_HTTP_INVALID); } } return validationResult; }, - validateParams: ( + validateParams: async ( actionParams: TeamsActionParams - ): GenericValidationResult => { + ): Promise> => { + const translations = await import('./translations'); const errors = { message: new Array(), }; const validationResult = { errors }; if (!actionParams.message?.length) { - errors.message.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredMessageText', - { - defaultMessage: 'Message is required.', - } - ) - ); + errors.message.push(translations.MESSAGE_REQUIRED); } return validationResult; }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.tsx index 454b93869222..8de1c68926f1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.tsx @@ -20,6 +20,9 @@ const TeamsActionFields: React.FunctionComponent< const { webhookUrl } = action.secrets; const { docLinks } = useKibana().services; + const isWebhookUrlInvalid: boolean = + errors.webhookUrl !== undefined && errors.webhookUrl.length > 0 && webhookUrl !== undefined; + return ( <> } error={errors.webhookUrl} - isInvalid={errors.webhookUrl.length > 0 && webhookUrl !== undefined} + isInvalid={isWebhookUrlInvalid} label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlTextFieldLabel', { @@ -54,7 +57,7 @@ const TeamsActionFields: React.FunctionComponent< )} 0 && webhookUrl !== undefined} + isInvalid={isWebhookUrlInvalid} name="webhookUrl" readOnly={readOnly} value={webhookUrl || ''} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_params.tsx index c0a20e214b4e..0aea576c10b3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_params.tsx @@ -40,7 +40,7 @@ const TeamsParamsFields: React.FunctionComponent ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/translations.ts new file mode 100644 index 000000000000..790a3b3bac32 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/translations.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const WEBHOOK_URL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredWebhookUrlText', + { + defaultMessage: 'Webhook URL is required.', + } +); + +export const WEBHOOK_URL_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.invalidWebhookUrlText', + { + defaultMessage: 'Webhook URL is invalid.', + } +); + +export const WEBHOOK_URL_HTTP_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requireHttpsWebhookUrlText', + { + defaultMessage: 'Webhook URL must start with https://.', + } +); + +export const MESSAGE_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredMessageText', + { + defaultMessage: 'Message is required.', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/translations.ts new file mode 100644 index 000000000000..3550121e8169 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/translations.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const URL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.requiredUrlText', + { + defaultMessage: 'URL is required.', + } +); + +export const URL_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.invalidUrlTextField', + { + defaultMessage: 'URL is invalid.', + } +); + +export const METHOD_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText', + { + defaultMessage: 'Method is required.', + } +); + +export const USERNAME_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthUserNameText', + { + defaultMessage: 'Username is required.', + } +); + +export const PASSWORD_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthPasswordText', + { + defaultMessage: 'Password is required.', + } +); + +export const PASSWORD_REQUIRED_FOR_USER = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText', + { + defaultMessage: 'Password is required when username is used.', + } +); + +export const USERNAME_REQUIRED_FOR_PASSWORD = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredUserText', + { + defaultMessage: 'Username is required when password is used.', + } +); + +export const BODY_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText', + { + defaultMessage: 'Body is required.', + } +); 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 index 8399316044f3..3e42e7965c5b 100644 --- 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 @@ -30,7 +30,7 @@ describe('actionTypeRegistry.get() works', () => { }); describe('webhook connector validation', () => { - test('connector validation succeeds when hasAuth is true and connector config is valid', () => { + test('connector validation succeeds when hasAuth is true and connector config is valid', async () => { const actionConnector = { secrets: { user: 'user', @@ -48,7 +48,7 @@ describe('webhook connector validation', () => { }, } as WebhookActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { url: [], @@ -64,7 +64,7 @@ describe('webhook connector validation', () => { }); }); - test('connector validation succeeds when hasAuth is false and connector config is valid', () => { + test('connector validation succeeds when hasAuth is false and connector config is valid', async () => { const actionConnector = { secrets: { user: '', @@ -82,7 +82,7 @@ describe('webhook connector validation', () => { }, } as WebhookActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { url: [], @@ -98,7 +98,7 @@ describe('webhook connector validation', () => { }); }); - test('connector validation fails when connector config is not valid', () => { + test('connector validation fails when connector config is not valid', async () => { const actionConnector = { secrets: { user: 'user', @@ -112,7 +112,7 @@ describe('webhook connector validation', () => { }, } as WebhookActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { url: ['URL is required.'], @@ -128,7 +128,7 @@ describe('webhook connector validation', () => { }); }); - test('connector validation fails when url in config is not valid', () => { + test('connector validation fails when url in config is not valid', async () => { const actionConnector = { secrets: { user: 'user', @@ -144,7 +144,7 @@ describe('webhook connector validation', () => { }, } as WebhookActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { url: ['URL is invalid.'], @@ -162,22 +162,22 @@ describe('webhook connector validation', () => { }); describe('webhook action params validation', () => { - test('action params validation succeeds when action params is valid', () => { + test('action params validation succeeds when action params is valid', async () => { const actionParams = { body: 'message {test}', }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { body: [] }, }); }); - test('params validation fails when body is not valid', () => { + test('params validation fails when body is not valid', async () => { const actionParams = { body: '', }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await 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 index 3ba801b83c46..a668f531a6d4 100644 --- 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 @@ -40,9 +40,12 @@ export function getActionType(): ActionTypeModel< defaultMessage: 'Webhook data', } ), - validateConnector: ( + validateConnector: async ( action: WebhookActionConnector - ): ConnectorValidationResult, WebhookSecrets> => { + ): Promise< + ConnectorValidationResult, WebhookSecrets> + > => { + const translations = await import('./translations'); const configErrors = { url: new Array(), method: new Array(), @@ -56,95 +59,39 @@ export function getActionType(): ActionTypeModel< secrets: { errors: secretsErrors }, }; if (!action.config.url) { - configErrors.url.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.requiredUrlText', - { - defaultMessage: 'URL is required.', - } - ) - ); + configErrors.url.push(translations.URL_REQUIRED); } if (action.config.url && !isValidUrl(action.config.url)) { - configErrors.url = [ - ...configErrors.url, - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.invalidUrlTextField', - { - defaultMessage: 'URL is invalid.', - } - ), - ]; + configErrors.url = [...configErrors.url, translations.URL_INVALID]; } if (!action.config.method) { - configErrors.method.push( - i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText', - { - defaultMessage: 'Method is required.', - } - ) - ); + configErrors.method.push(translations.METHOD_REQUIRED); } if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) { - secretsErrors.user.push( - i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthUserNameText', - { - defaultMessage: 'Username is required.', - } - ) - ); + secretsErrors.user.push(translations.USERNAME_REQUIRED); } if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) { - secretsErrors.password.push( - i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthPasswordText', - { - defaultMessage: 'Password is required.', - } - ) - ); + secretsErrors.password.push(translations.PASSWORD_REQUIRED); } if (action.secrets.user && !action.secrets.password) { - secretsErrors.password.push( - i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText', - { - defaultMessage: 'Password is required when username is used.', - } - ) - ); + secretsErrors.password.push(translations.PASSWORD_REQUIRED_FOR_USER); } if (!action.secrets.user && action.secrets.password) { - secretsErrors.user.push( - i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredUserText', - { - defaultMessage: 'Username is required when password is used.', - } - ) - ); + secretsErrors.user.push(translations.USERNAME_REQUIRED_FOR_PASSWORD); } return validationResult; }, - validateParams: ( + validateParams: async ( actionParams: WebhookActionParams - ): GenericValidationResult => { + ): Promise> => { + const translations = await import('./translations'); const errors = { body: new Array(), }; const validationResult = { errors }; validationResult.errors = errors; if (!actionParams.body?.length) { - errors.body.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText', - { - defaultMessage: 'Body is required.', - } - ) - ); + errors.body.push(translations.BODY_REQUIRED); } return validationResult; }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx index d3231f52b4d7..ba0e7016caa7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx @@ -76,7 +76,11 @@ const WebhookActionConnectorFields: React.FunctionComponent< ) ); } - const hasHeaderErrors = headerErrors.keyHeader.length > 0 || headerErrors.valueHeader.length > 0; + const hasHeaderErrors: boolean = + (headerErrors.keyHeader !== undefined && + headerErrors.valueHeader !== undefined && + headerErrors.keyHeader.length > 0) || + headerErrors.valueHeader.length > 0; function addHeader() { if (headers && !!Object.keys(headers).find((key) => key === httpHeaderKey)) { @@ -219,6 +223,13 @@ const WebhookActionConnectorFields: React.FunctionComponent< ); }); + const isUrlInvalid: boolean = + errors.url !== undefined && errors.url.length > 0 && url !== undefined; + const isPasswordInvalid: boolean = + password !== undefined && errors.password !== undefined && errors.password.length > 0; + const isUserInvalid: boolean = + user !== undefined && errors.user !== undefined && errors.user.length > 0; + return ( <> @@ -248,7 +259,7 @@ const WebhookActionConnectorFields: React.FunctionComponent< id="url" fullWidth error={errors.url} - isInvalid={errors.url.length > 0 && url !== undefined} + isInvalid={isUrlInvalid} label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.urlTextFieldLabel', { @@ -258,7 +269,7 @@ const WebhookActionConnectorFields: React.FunctionComponent< > 0 && url !== undefined} + isInvalid={isUrlInvalid} fullWidth readOnly={readOnly} value={url || ''} @@ -326,7 +337,7 @@ const WebhookActionConnectorFields: React.FunctionComponent< id="webhookUser" fullWidth error={errors.user} - isInvalid={errors.user.length > 0 && user !== undefined} + isInvalid={isUserInvalid} label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.userTextFieldLabel', { @@ -336,7 +347,7 @@ const WebhookActionConnectorFields: React.FunctionComponent< > 0 && user !== undefined} + isInvalid={isUserInvalid} name="user" readOnly={readOnly} value={user || ''} @@ -357,7 +368,7 @@ const WebhookActionConnectorFields: React.FunctionComponent< id="webhookPassword" fullWidth error={errors.password} - isInvalid={errors.password.length > 0 && password !== undefined} + isInvalid={isPasswordInvalid} label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.passwordTextFieldLabel', { @@ -369,7 +380,7 @@ const WebhookActionConnectorFields: React.FunctionComponent< fullWidth name="password" readOnly={readOnly} - isInvalid={errors.password.length > 0 && password !== undefined} + isInvalid={isPasswordInvalid} value={password || ''} data-test-subj="webhookPasswordInput" onChange={(e) => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx index 964f538d5497..091ea1e305e3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx @@ -23,12 +23,12 @@ describe('action_connector_form', () => { id: 'my-action-type', iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, }); actionTypeRegistry.get.mockReturnValue(actionType); 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 0790dce9ca3d..29232940da5c 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 @@ -51,11 +51,11 @@ export function validateBaseProperties( return validationResult; } -export function getConnectorErrors( +export async function getConnectorErrors( connector: UserConfiguredActionConnector, actionTypeModel: ActionTypeModel ) { - const connectorValidationResult = actionTypeModel?.validateConnector(connector); + const connectorValidationResult = await actionTypeModel?.validateConnector(connector); const configErrors = (connectorValidationResult.config ? connectorValidationResult.config.errors : {}) as IErrorObject; @@ -173,7 +173,8 @@ export const ActionConnectorForm = ({ ); const FieldsComponent = actionTypeRegistered.actionConnectorFields; - + const isNameInvalid: boolean = + connector.name !== undefined && errors.name !== undefined && errors.name.length > 0; return ( } - isInvalid={errors.name.length > 0 && connector.name !== undefined} + isInvalid={isNameInvalid} error={errors.name} > 0 && connector.name !== undefined} + isInvalid={isNameInvalid} name="name" placeholder="Untitled" data-test-subj="nameInput" diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index ad727be58280..bedde696e51c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -53,12 +53,12 @@ describe('action_form', () => { id: 'my-action-type', iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, actionParamsFields: mockedActionParamsFields, @@ -68,12 +68,12 @@ describe('action_form', () => { id: 'disabled-by-config', iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, actionParamsFields: mockedActionParamsFields, @@ -83,12 +83,12 @@ describe('action_form', () => { id: '.jira', iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): ValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, actionParamsFields: mockedActionParamsFields, @@ -98,12 +98,12 @@ describe('action_form', () => { id: 'disabled-by-license', iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, actionParamsFields: mockedActionParamsFields, @@ -113,12 +113,12 @@ describe('action_form', () => { id: 'preconfigured', iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, actionParamsFields: mockedActionParamsFields, 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 e9f79633ef52..f12ce25abc49 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 @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -30,7 +30,7 @@ import { ActionTypeRegistryContract, } from '../../../types'; import { SectionLoading } from '../../components/section_loading'; -import { ActionTypeForm, ActionTypeFormProps } from './action_type_form'; +import { ActionTypeForm } from './action_type_form'; import { AddConnectorInline } from './connector_add_inline'; import { actionTypeCompare } from '../../lib/action_type_compare'; import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled'; @@ -357,49 +357,42 @@ export const ActionForm = ({ ); } - const actionParamsErrors: ActionTypeFormProps['actionParamsErrors'] = actionTypeRegistry - .get(actionItem.actionTypeId) - ?.validateParams(actionItem.params); - return ( - - { - setActiveActionItem({ actionTypeId: actionItem.actionTypeId, indices: [index] }); - setAddModalVisibility(true); - }} - onConnectorSelected={(id: string) => { - setActionIdByIndex(id, index); - }} - actionTypeRegistry={actionTypeRegistry} - onDeleteAction={() => { - const updatedActions = actions.filter( - (_item: AlertAction, i: number) => i !== index - ); - setActions(updatedActions); - setIsAddActionPanelOpen( - updatedActions.filter((item: AlertAction) => item.id !== actionItem.id) - .length === 0 - ); - setActiveActionItem(undefined); - }} - /> - - + { + setActiveActionItem({ actionTypeId: actionItem.actionTypeId, indices: [index] }); + setAddModalVisibility(true); + }} + onConnectorSelected={(id: string) => { + setActionIdByIndex(id, index); + }} + actionTypeRegistry={actionTypeRegistry} + onDeleteAction={() => { + const updatedActions = actions.filter( + (_item: AlertAction, i: number) => i !== index + ); + setActions(updatedActions); + setIsAddActionPanelOpen( + updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length === + 0 + ); + setActiveActionItem(undefined); + }} + /> ); })} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx index 38f1e8f52254..e8590595b9d6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx @@ -43,12 +43,12 @@ describe('action_type_form', () => { id: '.pagerduty', iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, actionParamsFields: mockedActionParamsFields, @@ -92,12 +92,12 @@ describe('action_type_form', () => { id: '.pagerduty', iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, actionParamsFields: mockedActionParamsFields, @@ -220,7 +220,6 @@ function getActionTypeForm( onAddConnector={onAddConnector ?? jest.fn()} onDeleteAction={onDeleteAction ?? jest.fn()} onConnectorSelected={onConnectorSelected ?? jest.fn()} - actionParamsErrors={{ errors: { summary: [], timestamp: [], dedupKey: [] } }} defaultActionGroupId={defaultActionGroupId ?? 'default'} setActionParamsProperty={jest.fn()} index={index ?? 1} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx index 2690aeaffad3..526d899b7efb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx @@ -47,9 +47,6 @@ import { DefaultActionParams } from '../../lib/get_defaults_for_action_params'; export type ActionTypeFormProps = { actionItem: AlertAction; actionConnector: ActionConnector; - actionParamsErrors: { - errors: IErrorObject; - }; index: number; onAddConnector: () => void; onConnectorSelected: (id: string) => void; @@ -80,7 +77,6 @@ const preconfiguredMessage = i18n.translate( export const ActionTypeForm = ({ actionItem, actionConnector, - actionParamsErrors, index, onAddConnector, onConnectorSelected, @@ -106,6 +102,9 @@ export const ActionTypeForm = ({ const selectedActionGroup = actionGroups?.find(({ id }) => id === actionItem.group) ?? defaultActionGroup; const [actionGroup, setActionGroup] = useState(); + const [actionParamsErrors, setActionParamsErrors] = useState<{ errors: IErrorObject }>({ + errors: {}, + }); useEffect(() => { setAvailableActionVariables( @@ -130,6 +129,16 @@ export const ActionTypeForm = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [actionGroup]); + useEffect(() => { + (async () => { + const res: { errors: IErrorObject } = await actionTypeRegistry + .get(actionItem.actionTypeId) + ?.validateParams(actionItem.params); + setActionParamsErrors(res); + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionItem]); + const canSave = hasSaveActionsCapability(capabilities); const getSelectedOptions = (actionItemId: string) => { const selectedConnector = connectors.find((connector) => connector.id === actionItemId); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx index 9a011823612c..e15916138af7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx @@ -40,12 +40,12 @@ describe('connector_add_flyout', () => { id: 'my-action-type', iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, }); @@ -77,12 +77,12 @@ describe('connector_add_flyout', () => { id: 'my-action-type', iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, }); @@ -114,12 +114,12 @@ describe('connector_add_flyout', () => { id: 'my-action-type', iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx index fedb2ed38299..8dbe5f105a0f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx @@ -198,12 +198,12 @@ function createActionType() { id: `my-action-type-${++count}`, iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index d3a6d662720c..1a3a186d891c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useState, useReducer } from 'react'; +import React, { useCallback, useState, useReducer, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, @@ -30,7 +30,9 @@ import { ActionType, ActionConnector, UserConfiguredActionConnector, + IErrorObject, ConnectorAddFlyoutProps, + ActionTypeModel, } from '../../../types'; import { hasSaveActionsCapability } from '../../lib/capabilities'; import { createActionConnector } from '../../lib/action_connector_api'; @@ -38,6 +40,7 @@ import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; import { useKibana } from '../../../common/lib/kibana'; import { createConnectorReducer, InitialConnector, ConnectorReducer } from './connector_reducer'; import { getConnectorWithInvalidatedFields } from '../../lib/value_validators'; +import { CenterJustifiedSpinner } from '../../components/center_justified_spinner'; const ConnectorAddFlyout: React.FunctionComponent = ({ onClose, @@ -47,7 +50,9 @@ const ConnectorAddFlyout: React.FunctionComponent = ({ consumer, actionTypeRegistry, }) => { - let hasErrors = false; + const [hasErrors, setHasErrors] = useState(true); + let actionTypeModel: ActionTypeModel | undefined; + const { http, notifications: { toasts }, @@ -55,7 +60,17 @@ const ConnectorAddFlyout: React.FunctionComponent = ({ } = useKibana().services; const [actionType, setActionType] = useState(undefined); const [hasActionsUpgradeableByTrial, setHasActionsUpgradeableByTrial] = useState(false); - + const [errors, setErrors] = useState<{ + configErrors: IErrorObject; + connectorBaseErrors: IErrorObject; + connectorErrors: IErrorObject; + secretsErrors: IErrorObject; + }>({ + configErrors: {}, + connectorBaseErrors: {}, + connectorErrors: {}, + secretsErrors: {}, + }); // hooks const initialConnector: InitialConnector, Record> = { actionTypeId: actionType?.id ?? '', @@ -73,6 +88,24 @@ const ConnectorAddFlyout: React.FunctionComponent = ({ Record >, }); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + (async () => { + if (actionTypeModel) { + setIsLoading(true); + const res = await getConnectorErrors(connector, actionTypeModel); + setHasErrors( + !!Object.keys(res.connectorErrors).find( + (errorKey) => (res.connectorErrors as IErrorObject)[errorKey].length >= 1 + ) + ); + setIsLoading(false); + setErrors({ ...res }); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [connector, actionType]); const setActionProperty = ( key: Key, @@ -101,7 +134,6 @@ const ConnectorAddFlyout: React.FunctionComponent = ({ } let currentForm; - let actionTypeModel; let saveButton; if (!actionType) { currentForm = ( @@ -115,22 +147,12 @@ const ConnectorAddFlyout: React.FunctionComponent = ({ } else { actionTypeModel = actionTypeRegistry.get(actionType.id); - const { - configErrors, - connectorBaseErrors, - connectorErrors, - secretsErrors, - } = getConnectorErrors(connector, actionTypeModel); - hasErrors = !!Object.keys(connectorErrors).find( - (errorKey) => connectorErrors[errorKey].length >= 1 - ); - currentForm = ( @@ -170,9 +192,9 @@ const ConnectorAddFlyout: React.FunctionComponent = ({ setConnector( getConnectorWithInvalidatedFields( connector, - configErrors, - secretsErrors, - connectorBaseErrors + errors.configErrors, + errors.secretsErrors, + errors.connectorBaseErrors ) ); return; @@ -235,13 +257,13 @@ const ConnectorAddFlyout: React.FunctionComponent = ({ - {actionTypeModel && actionTypeModel.iconClass ? ( + {!!actionTypeModel && actionTypeModel.iconClass ? ( ) : null} - {actionTypeModel && actionType ? ( + {!!actionTypeModel && actionType ? ( <>

@@ -280,7 +302,17 @@ const ConnectorAddFlyout: React.FunctionComponent = ({ ) } > - {currentForm} + <> + {currentForm} + {isLoading ? ( + <> + + {' '} + + ) : ( + <> + )} + @@ -314,7 +346,7 @@ const ConnectorAddFlyout: React.FunctionComponent = ({ - {canSave && actionTypeModel && actionType ? saveButton : null} + {canSave && !!actionTypeModel && actionType ? saveButton : null} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx index c18f6955d121..1ae37cf96cd3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx @@ -39,12 +39,12 @@ describe('connector_add_modal', () => { id: 'my-action-type', iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx index d01ee08df239..1e9669d1995d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useMemo, useReducer, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiModal, @@ -19,6 +19,7 @@ import { EuiFlexItem, EuiIcon, EuiFlexGroup, + EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ActionConnectorForm, getConnectorErrors } from './action_connector_form'; @@ -31,9 +32,11 @@ import { ActionConnector, ActionTypeRegistryContract, UserConfiguredActionConnector, + IErrorObject, } from '../../../types'; import { useKibana } from '../../../common/lib/kibana'; import { getConnectorWithInvalidatedFields } from '../../lib/value_validators'; +import { CenterJustifiedSpinner } from '../../components/center_justified_spinner'; // eslint-disable-next-line @typescript-eslint/consistent-type-definitions type ConnectorAddModalProps = { @@ -56,7 +59,7 @@ const ConnectorAddModal = ({ notifications: { toasts }, application: { capabilities }, } = useKibana().services; - let hasErrors = false; + const [hasErrors, setHasErrors] = useState(true); const initialConnector: InitialConnector< Record, Record @@ -69,6 +72,7 @@ const ConnectorAddModal = ({ [actionType.id] ); const [isSaving, setIsSaving] = useState(false); + const [isLoading, setIsLoading] = useState(false); const canSave = hasSaveActionsCapability(capabilities); const reducer: ConnectorReducer< @@ -81,6 +85,34 @@ const ConnectorAddModal = ({ Record >, }); + const [errors, setErrors] = useState<{ + configErrors: IErrorObject; + connectorBaseErrors: IErrorObject; + connectorErrors: IErrorObject; + secretsErrors: IErrorObject; + }>({ + configErrors: {}, + connectorBaseErrors: {}, + connectorErrors: {}, + secretsErrors: {}, + }); + + const actionTypeModel = actionTypeRegistry.get(actionType.id); + + useEffect(() => { + (async () => { + setIsLoading(true); + const res = await getConnectorErrors(connector, actionTypeModel); + setHasErrors( + !!Object.keys(res.connectorErrors).find( + (errorKey) => (res.connectorErrors as IErrorObject)[errorKey].length >= 1 + ) + ); + setIsLoading(false); + setErrors({ ...res }); + })(); + }, [connector, actionTypeModel]); + const setConnector = (value: any) => { dispatch({ command: { type: 'setConnector' }, payload: { key: 'connector', value } }); }; @@ -97,15 +129,6 @@ const ConnectorAddModal = ({ onClose(); }, [initialConnector, onClose]); - const actionTypeModel = actionTypeRegistry.get(actionType.id); - const { configErrors, connectorBaseErrors, connectorErrors, secretsErrors } = getConnectorErrors( - connector, - actionTypeModel - ); - hasErrors = !!Object.keys(connectorErrors).find( - (errorKey) => connectorErrors[errorKey].length >= 1 - ); - const onActionConnectorSave = async (): Promise => await createActionConnector({ http, connector }) .then((savedConnector) => { @@ -157,15 +180,25 @@ const ConnectorAddModal = ({ - + <> + + {isLoading ? ( + <> + + {' '} + + ) : ( + <> + )} + @@ -189,9 +222,9 @@ const ConnectorAddModal = ({ setConnector( getConnectorWithInvalidatedFields( connector, - configErrors, - secretsErrors, - connectorBaseErrors + errors.configErrors, + errors.secretsErrors, + errors.connectorBaseErrors ) ); return; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx index 56bf57cb4509..e6d3c0bde811 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx @@ -51,12 +51,12 @@ describe('connector_edit_flyout', () => { id: 'test-action-type-id', iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, }); @@ -95,12 +95,12 @@ describe('connector_edit_flyout', () => { id: 'test-action-type-id', iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index 66a4dcc452c5..ca729f9a6166 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useReducer, useState } from 'react'; +import React, { useCallback, useReducer, useState, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, @@ -23,6 +23,7 @@ import { EuiLink, EuiTabs, EuiTab, + EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Option, none, some } from 'fp-ts/lib/Option'; @@ -31,6 +32,7 @@ import { TestConnectorForm } from './test_connector_form'; import { ActionConnector, ConnectorEditFlyoutProps, + IErrorObject, EditConectorTabs, UserConfiguredActionConnector, } from '../../../types'; @@ -44,6 +46,7 @@ import { import './connector_edit_flyout.scss'; import { useKibana } from '../../../common/lib/kibana'; import { getConnectorWithInvalidatedFields } from '../../lib/value_validators'; +import { CenterJustifiedSpinner } from '../../components/center_justified_spinner'; const ConnectorEditFlyout = ({ initialConnector, @@ -53,12 +56,14 @@ const ConnectorEditFlyout = ({ consumer, actionTypeRegistry, }: ConnectorEditFlyoutProps) => { + const [hasErrors, setHasErrors] = useState(true); const { http, notifications: { toasts }, docLinks, application: { capabilities }, } = useKibana().services; + const getConnectorWithoutSecrets = () => ({ ...(initialConnector as UserConfiguredActionConnector< Record, @@ -75,6 +80,35 @@ const ConnectorEditFlyout = ({ const [{ connector }, dispatch] = useReducer(reducer, { connector: getConnectorWithoutSecrets(), }); + const [isLoading, setIsLoading] = useState(false); + const [errors, setErrors] = useState<{ + configErrors: IErrorObject; + connectorBaseErrors: IErrorObject; + connectorErrors: IErrorObject; + secretsErrors: IErrorObject; + }>({ + configErrors: {}, + connectorBaseErrors: {}, + connectorErrors: {}, + secretsErrors: {}, + }); + + const actionTypeModel = actionTypeRegistry.get(connector.actionTypeId); + + useEffect(() => { + (async () => { + setIsLoading(true); + const res = await getConnectorErrors(connector, actionTypeModel); + setHasErrors( + !!Object.keys(res.connectorErrors).find( + (errorKey) => (res.connectorErrors as IErrorObject)[errorKey].length >= 1 + ) + ); + setIsLoading(false); + setErrors({ ...res }); + })(); + }, [connector, actionTypeModel]); + const [isSaving, setIsSaving] = useState(false); const [selectedTab, setTab] = useState(tab); @@ -113,25 +147,6 @@ const ConnectorEditFlyout = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [onClose]); - const actionTypeModel = actionTypeRegistry.get(connector.actionTypeId); - const { - configErrors, - connectorBaseErrors, - connectorErrors, - secretsErrors, - } = !connector.isPreconfigured - ? getConnectorErrors(connector, actionTypeModel) - : { - configErrors: {}, - connectorBaseErrors: {}, - connectorErrors: {}, - secretsErrors: {}, - }; - - const hasErrors = !!Object.keys(connectorErrors).find( - (errorKey) => connectorErrors[errorKey].length >= 1 - ); - const onActionConnectorSave = async (): Promise => await updateActionConnector({ http, connector, id: connector.id }) .then((savedConnector) => { @@ -227,9 +242,9 @@ const ConnectorEditFlyout = ({ setConnector( getConnectorWithInvalidatedFields( connector, - configErrors, - secretsErrors, - connectorBaseErrors + errors.configErrors, + errors.secretsErrors, + errors.connectorBaseErrors ) ); return; @@ -286,19 +301,29 @@ const ConnectorEditFlyout = ({ {selectedTab === EditConectorTabs.Configuration ? ( !connector.isPreconfigured ? ( - { - setHasChanges(true); - // if the user changes the connector, "forget" the last execution - // so the user comes back to a clean form ready to run a fresh test - setTestExecutionResult(none); - dispatch(changes); - }} - actionTypeRegistry={actionTypeRegistry} - consumer={consumer} - /> + <> + { + setHasChanges(true); + // if the user changes the connector, "forget" the last execution + // so the user comes back to a clean form ready to run a fresh test + setTestExecutionResult(none); + dispatch(changes); + }} + actionTypeRegistry={actionTypeRegistry} + consumer={consumer} + /> + {isLoading ? ( + <> + + {' '} + + ) : ( + <> + )} + ) : ( <> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.test.tsx index 5cdc15ab0375..ae15670ce8ab 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.test.tsx @@ -53,12 +53,12 @@ const actionType = { id: 'my-action-type', iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, actionParamsFields: mockedActionParamsFields, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx index 92a17a2e4cfa..242c1c33d8d7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Suspense } from 'react'; +import React, { Suspense, useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -46,11 +46,18 @@ export const TestConnectorForm = ({ isExecutingAction, actionTypeRegistry, }: ConnectorAddFlyoutProps) => { + const [actionErrors, setActionErrors] = useState({}); + const [hasErrors, setHasErrors] = useState(false); const actionTypeModel = actionTypeRegistry.get(connector.actionTypeId); const ParamsFieldsComponent = actionTypeModel.actionParamsFields; - const actionErrors = actionTypeModel?.validateParams(actionParams).errors as IErrorObject; - const hasErrors = !!Object.values(actionErrors).find((errors) => errors.length > 0); + useEffect(() => { + (async () => { + const res = (await actionTypeModel?.validateParams(actionParams)).errors as IErrorObject; + setActionErrors({ ...res }); + setHasErrors(!!Object.values(res).find((errors) => errors.length > 0)); + })(); + }, [actionTypeModel, actionParams]); const steps = [ { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index 7b6453e705ec..90eadaf5f9b8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -162,12 +162,12 @@ describe('actions_connectors_list component with items', () => { id: 'test', iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, actionParamsFields: mockedActionParamsFields, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index cb43c168aa99..b40b7cbc1a38 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -135,12 +135,12 @@ describe('alert_add', () => { id: 'my-action-type', iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index a40f77998d6e..2d111d540523 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -15,9 +15,10 @@ import { AlertTypeParams, AlertUpdates, AlertFlyoutCloseReason, + IErrorObject, AlertAddProps, } from '../../../types'; -import { AlertForm, getAlertErrors, isValidAlert } from './alert_form'; +import { AlertForm, getAlertActionErrors, getAlertErrors, isValidAlert } from './alert_form'; import { alertReducer, InitialAlert, InitialAlertReducer } from './alert_reducer'; import { createAlert } from '../../lib/alert_api'; import { HealthCheck } from '../../components/health_check'; @@ -102,6 +103,18 @@ const AlertAdd = ({ } }, [alert.params, initialAlertParams, setInitialAlertParams]); + const [alertActionsErrors, setAlertActionsErrors] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + (async () => { + setIsLoading(true); + const res = await getAlertActionErrors(alert as Alert, actionTypeRegistry); + setIsLoading(false); + setAlertActionsErrors([...res]); + })(); + }, [alert, actionTypeRegistry]); + const checkForChangesAndCloseFlyout = () => { if ( hasAlertChanged(alert, initialAlert, false) || @@ -125,9 +138,8 @@ const AlertAdd = ({ }; const alertType = alert.alertTypeId ? alertTypeRegistry.get(alert.alertTypeId) : null; - const { alertActionsErrors, alertBaseErrors, alertErrors, alertParamsErrors } = getAlertErrors( + const { alertBaseErrors, alertErrors, alertParamsErrors } = getAlertErrors( alert as Alert, - actionTypeRegistry, alertType ); @@ -195,9 +207,10 @@ const AlertAdd = ({ { setIsSaving(true); - if (!isValidAlert(alert, alertErrors, alertActionsErrors)) { + if (isLoading || !isValidAlert(alert, alertErrors, alertActionsErrors)) { setAlert( getAlertWithInvalidatedFields( alert as Alert, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add_footer.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add_footer.tsx index fe4b9d066429..ee36257dedf0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add_footer.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add_footer.tsx @@ -13,17 +13,25 @@ import { EuiFlexItem, EuiButtonEmpty, EuiButton, + EuiLoadingSpinner, + EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useHealthContext } from '../../context/health_context'; interface AlertAddFooterProps { isSaving: boolean; + isFormLoading: boolean; onSave: () => void; onCancel: () => void; } -export const AlertAddFooter = ({ isSaving, onSave, onCancel }: AlertAddFooterProps) => { +export const AlertAddFooter = ({ + isSaving, + onSave, + onCancel, + isFormLoading, +}: AlertAddFooterProps) => { const { loadingHealthCheck } = useHealthContext(); return ( @@ -36,6 +44,14 @@ export const AlertAddFooter = ({ isSaving, onSave, onCancel }: AlertAddFooterPro })} + {isFormLoading ? ( + + + + + ) : ( + <> + )} { id: 'my-action-type', iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index f6569f32088e..bf6f0ef43b82 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useReducer, useState } from 'react'; +import React, { useReducer, useState, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, @@ -20,11 +20,12 @@ import { EuiPortal, EuiCallOut, EuiSpacer, + EuiLoadingSpinner, } from '@elastic/eui'; import { cloneDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { Alert, AlertEditProps, AlertFlyoutCloseReason } from '../../../types'; -import { AlertForm, getAlertErrors, isValidAlert } from './alert_form'; +import { Alert, AlertFlyoutCloseReason, AlertEditProps, IErrorObject } from '../../../types'; +import { AlertForm, getAlertActionErrors, getAlertErrors, isValidAlert } from './alert_form'; import { alertReducer, ConcreteAlertReducer } from './alert_reducer'; import { updateAlert } from '../../lib/alert_api'; import { HealthCheck } from '../../components/health_check'; @@ -53,6 +54,8 @@ export const AlertEdit = ({ false ); const [isConfirmAlertCloseModalOpen, setIsConfirmAlertCloseModalOpen] = useState(false); + const [alertActionsErrors, setAlertActionsErrors] = useState([]); + const [isLoading, setIsLoading] = useState(false); const { http, @@ -64,9 +67,17 @@ export const AlertEdit = ({ const alertType = alertTypeRegistry.get(alert.alertTypeId); - const { alertActionsErrors, alertBaseErrors, alertErrors, alertParamsErrors } = getAlertErrors( + useEffect(() => { + (async () => { + setIsLoading(true); + const res = await getAlertActionErrors(alert as Alert, actionTypeRegistry); + setAlertActionsErrors([...res]); + setIsLoading(false); + })(); + }, [alert, actionTypeRegistry]); + + const { alertBaseErrors, alertErrors, alertParamsErrors } = getAlertErrors( alert as Alert, - actionTypeRegistry, alertType ); @@ -80,7 +91,11 @@ export const AlertEdit = ({ async function onSaveAlert(): Promise { try { - if (isValidAlert(alert, alertErrors, alertActionsErrors) && !hasActionsWithBrokenConnector) { + if ( + !isLoading && + isValidAlert(alert, alertErrors, alertActionsErrors) && + !hasActionsWithBrokenConnector + ) { const newAlert = await updateAlert({ http, alert, id: alert.id }); toasts.addSuccess( i18n.translate('xpack.triggersActionsUI.sections.alertEdit.saveSuccessNotificationText', { @@ -177,6 +192,14 @@ export const AlertEdit = ({ )} + {isLoading ? ( + + + + + ) : ( + <> + )} { id: 'my-action-type', iconClass: 'test', selectMessage: 'test', - validateConnector: (): ConnectorValidationResult => { - return { + validateConnector: (): Promise> => { + return Promise.resolve({ config: { errors: {}, }, secrets: { errors: {}, }, - }; + }); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index b4b6477fd594..16878abc362d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -121,11 +121,7 @@ export function validateBaseProperties(alertObject: InitialAlert): ValidationRes return validationResult; } -export function getAlertErrors( - alert: Alert, - actionTypeRegistry: ActionTypeRegistryContract, - alertTypeModel: AlertTypeModel | null -) { +export function getAlertErrors(alert: Alert, alertTypeModel: AlertTypeModel | null) { const alertParamsErrors: IErrorObject = alertTypeModel ? alertTypeModel.validate(alert.params).errors : []; @@ -135,18 +131,26 @@ export function getAlertErrors( ...alertBaseErrors, } as IErrorObject; - const alertActionsErrors = alert.actions.map((alertAction: AlertAction) => { - return actionTypeRegistry.get(alertAction.actionTypeId)?.validateParams(alertAction.params) - .errors; - }); return { alertParamsErrors, alertBaseErrors, - alertActionsErrors, alertErrors, }; } +export async function getAlertActionErrors( + alert: Alert, + actionTypeRegistry: ActionTypeRegistryContract +): Promise { + return await Promise.all( + alert.actions.map( + async (alertAction: AlertAction) => + (await actionTypeRegistry.get(alertAction.actionTypeId)?.validateParams(alertAction.params)) + .errors + ) + ); +} + export const hasObjectErrors: (errors: IErrorObject) => boolean = (errors) => !!Object.values(errors).find((errorList) => { if (isObject(errorList)) return hasObjectErrors(errorList as IErrorObject); 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 8c7876c3f725..ee561a65069e 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 @@ -42,12 +42,12 @@ const getTestActionType = ( id: id || 'my-action-type', iconClass: iconClass || 'test', selectMessage: selectedMessage || 'test', - validateConnector: (): ConnectorValidationResult => { - return {}; + validateConnector: (): Promise> => { + return Promise.resolve({}); }, - validateParams: (): GenericValidationResult => { + validateParams: (): Promise> => { const validationResult = { errors: {} }; - return validationResult; + return Promise.resolve(validationResult); }, actionConnectorFields: null, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 0f2b961b1f2d..5ddddcb73a84 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -109,10 +109,10 @@ export interface ActionTypeModel - ) => ConnectorValidationResult, Partial>; + ) => Promise, Partial>>; validateParams: ( actionParams: ActionParams - ) => GenericValidationResult | unknown>; + ) => Promise | unknown>>; actionConnectorFields: React.LazyExoticComponent< ComponentType< ActionConnectorFieldsProps>