From 85fe84e51d280c303289378869eb877cfb880698 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 4 Jan 2021 13:52:01 -0600 Subject: [PATCH 01/96] Adding swimlane connector --- .../server/builtin_action_types/index.test.ts | 1 + .../server/builtin_action_types/index.ts | 2 + .../builtin_action_types/swimlane/api.test.ts | 40 +++ .../builtin_action_types/swimlane/api.ts | 19 ++ .../swimlane/helpers.test.ts | 75 ++++++ .../builtin_action_types/swimlane/helpers.ts | 54 +++++ .../builtin_action_types/swimlane/index.ts | 117 +++++++++ .../builtin_action_types/swimlane/mocks.ts | 41 ++++ .../builtin_action_types/swimlane/schema.ts | 66 +++++ .../swimlane/service.test.ts | 143 +++++++++++ .../builtin_action_types/swimlane/service.ts | 84 +++++++ .../swimlane/translations.ts | 20 ++ .../builtin_action_types/swimlane/types.ts | 91 +++++++ .../swimlane/validators.ts | 28 +++ .../server/usage/actions_usage_collector.ts | 1 + .../security_solution/common/constants.ts | 1 + .../schema/xpack_plugins.json | 6 + .../components/builtin_action_types/index.ts | 2 + .../builtin_action_types/swimlane/api.ts | 40 +++ .../builtin_action_types/swimlane/index.ts | 8 + .../swimlane/steps/index.ts | 23 ++ .../swimlane/steps/swimlane_connection.tsx | 164 +++++++++++++ .../swimlane/steps/swimlane_fields.tsx | 146 +++++++++++ .../swimlane/swimlane.svg | 8 + .../swimlane/swimlane.test.tsx | 116 +++++++++ .../swimlane/swimlane.tsx | 89 +++++++ .../swimlane/swimlane_connectors.test.tsx | 111 +++++++++ .../swimlane/swimlane_connectors.tsx | 72 ++++++ .../swimlane/swimlane_params.test.tsx | 47 ++++ .../swimlane/swimlane_params.tsx | 155 ++++++++++++ .../swimlane/translations.ts | 175 ++++++++++++++ .../builtin_action_types/swimlane/types.ts | 51 ++++ .../actions/builtin_action_types/swimlane.ts | 39 +++ .../basic/tests/actions/index.ts | 1 + .../alerting_api_integration/common/config.ts | 1 + .../actions_simulators/server/README.md | 2 +- .../actions_simulators/server/plugin.ts | 3 + .../server/swimlane_simulation.ts | 50 ++++ .../actions/builtin_action_types/swimlane.ts | 227 ++++++++++++++++++ .../tests/actions/index.ts | 1 + .../case_api_integration/common/config.ts | 1 + .../common/config.ts | 1 + x-pack/test/functional_with_es_ssl/config.ts | 1 + 43 files changed, 2322 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/translations.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/validators.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/index.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/index.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.svg create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts create mode 100644 x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts create mode 100644 x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.ts create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts index 10955af2f3b13..5feb47ea6c962 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts @@ -21,6 +21,7 @@ const ACTION_TYPE_IDS = [ '.pagerduty', '.server-log', '.slack', + '.swimlane', '.teams', '.webhook', ]; diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts index 551d3d02ff05d..07859cba4c371 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts @@ -12,6 +12,7 @@ import { Logger } from '../../../../../src/core/server'; import { getActionType as getEmailActionType } from './email'; import { getActionType as getIndexActionType } from './es_index'; import { getActionType as getPagerDutyActionType } from './pagerduty'; +import { getActionType as getSwimlaneActionType } from './swimlane'; import { getActionType as getServerLogActionType } from './server_log'; import { getActionType as getSlackActionType } from './slack'; import { getActionType as getWebhookActionType } from './webhook'; @@ -65,6 +66,7 @@ export function registerBuiltInActionTypes({ ); actionTypeRegistry.register(getIndexActionType({ logger })); actionTypeRegistry.register(getPagerDutyActionType({ logger, configurationUtilities })); + actionTypeRegistry.register(getSwimlaneActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getServerLogActionType({ logger })); actionTypeRegistry.register(getSlackActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities })); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts new file mode 100644 index 0000000000000..17e7e1096c470 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts @@ -0,0 +1,40 @@ +/* + * 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 { api } from './api'; +import { ExternalService } from './types'; +import { externalServiceMock } from './mocks'; +import { Logger } from '@kbn/logging'; +let mockedLogger: jest.Mocked; + +describe('api', () => { + let externalService: jest.Mocked; + + beforeEach(() => { + externalService = externalServiceMock.create(); + }); + + describe('createRecord', () => { + test('it creates a record correctly', async () => { + const res = await api.createRecord({ + externalService, + logger: mockedLogger, + params: { + alertName: 'alert name', + caseName: 'case name', + severity: 'critical', + alertSource: 'elastic', + caseId: '123456', + comments: 'some comments', + }, + }); + expect(res).toEqual({ + id: '123456', + }); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts new file mode 100644 index 0000000000000..8efc1927f5351 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts @@ -0,0 +1,19 @@ +/* + * 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 { CreateRecordApiHandlerArgs, CreateRecordResponse, ExternalServiceApi } from './types'; + +const createRecordHandler = async ({ + externalService, + params, +}: CreateRecordApiHandlerArgs): Promise => { + return await externalService.createRecord(params); +}; + +export const api: ExternalServiceApi = { + createRecord: createRecordHandler, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts new file mode 100644 index 0000000000000..002331a1f32ce --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts @@ -0,0 +1,75 @@ +/* + * 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 { MappingConfigType } from './types'; +import { getBodyForEventAction } from './helpers'; + +describe('Create Record Mapping', () => { + let mappingConfig: MappingConfigType; + const appId = '45678'; + + beforeAll(() => { + mappingConfig = { + alertSourceConfig: { + id: 'adnjls', + name: 'Alert Source', + key: 'alert-source', + fieldType: 'text', + }, + severityConfig: { + id: 'adnlas', + name: 'Severity', + key: 'severity', + fieldType: 'text', + }, + alertNameConfig: { + id: 'adnfls', + name: 'Alert Name', + key: 'alert-name', + fieldType: 'text', + }, + caseIdConfig: { + id: 'a6sst', + name: 'Case Id', + key: 'case-id-name', + fieldType: 'text', + }, + caseNameConfig: { + id: 'a6fst', + name: 'Case Name', + key: 'case-name', + fieldType: 'text', + }, + commentsConfig: { + id: 'a6fdf', + name: 'Comments', + key: 'comments', + fieldType: 'text', + }, + }; + }); + + test('Mapping is Successful', () => { + const params = { + alertName: 'Alert Name', + severity: 'Critical', + alertSource: 'Elastic', + caseName: 'Case Name', + caseId: 'es3456789', + comments: 'This is a comment', + }; + const data = getBodyForEventAction(appId, mappingConfig, params); + expect(data?.values?.[mappingConfig.alertSourceConfig.id]).toEqual(params.alertSource); + expect(data?.values?.[mappingConfig.alertNameConfig.id]).toEqual(params.alertName); + // @ts-ignore + expect(data?.values?.[mappingConfig.caseNameConfig.id]).toEqual(params.caseName); + expect(data?.values?.[mappingConfig.caseIdConfig.id]).toEqual(params.caseId); + // @ts-ignore + expect(data?.values?.[mappingConfig.commentsConfig.id]).toEqual(params.comments); + expect(data?.values?.[mappingConfig.severityConfig.id]).toEqual(params.severity); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts new file mode 100644 index 0000000000000..7934a59f5031a --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CreateRecordParams, MappingConfigType, SwimlaneRecordPayload } from './types'; + +export const getBodyForEventAction = ( + applicationId: string, + mappingConfig: MappingConfigType, + params: CreateRecordParams +): SwimlaneRecordPayload => { + const data: SwimlaneRecordPayload = { + applicationId, + }; + + const values: Record = {}; + + for (const mappingsKey in mappingConfig) { + if (!Object.hasOwnProperty.call(mappingConfig, mappingsKey)) { + continue; + } + + const fieldMap = mappingConfig[mappingsKey]; + + if (!fieldMap) { + continue; + } + + const { id, fieldType } = fieldMap; + const paramName = mappingsKey.replace('Config', ''); + if (params[paramName]) { + const value = params[paramName]; + if (value) { + switch (fieldType) { + case 'numeric': { + values[id] = +value; + break; + } + default: { + values[id] = value; + break; + } + } + } + } + } + + data.values = values; + + return data; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts new file mode 100644 index 0000000000000..adf04d86de1be --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { curry } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { Logger } from '@kbn/logging'; +import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { + SwimlaneExecutorResultData, + SwimlanePublicConfigurationType, + SwimlaneSecretConfigurationType, + ExecutorParams, + ExecutorSubActionCreateRecordParams, +} from './types'; +import { validate } from './validators'; +import { + ExecutorParamsSchema, + SwimlaneSecretsConfiguration, + SwimlaneServiceConfiguration, +} from './schema'; +import { createExternalService } from './service'; +import { api } from './api'; + +interface GetActionTypeParams { + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; +} + +const supportedSubActions: string[] = ['application', 'createRecord']; + +// action type definition +export function getActionType( + params: GetActionTypeParams +): ActionType< + SwimlanePublicConfigurationType, + SwimlaneSecretConfigurationType, + ExecutorParams, + SwimlaneExecutorResultData | {} +> { + const { logger, configurationUtilities } = params; + + return { + id: '.swimlane', + minimumLicenseRequired: 'gold', + name: i18n.translate('xpack.actions.builtin.swimlaneTitle', { + defaultMessage: 'Swimlane', + }), + validate: { + config: schema.object(SwimlaneServiceConfiguration, { + validate: curry(validate.config)(configurationUtilities), + }), + secrets: schema.object(SwimlaneSecretsConfiguration, { + validate: curry(validate.secrets)(configurationUtilities), + }), + params: ExecutorParamsSchema, + }, + executor: curry(executor)({ logger, configurationUtilities }), + }; +} + +async function executor( + { + logger, + configurationUtilities, + }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities }, + execOptions: ActionTypeExecutorOptions< + SwimlanePublicConfigurationType, + SwimlaneSecretConfigurationType, + ExecutorParams + > +): Promise> { + const { actionId, config, params, secrets } = execOptions; + const { subAction, subActionParams } = params as ExecutorParams; + + let data: SwimlaneExecutorResultData | null = null; + + const externalService = createExternalService( + { + config, + secrets, + }, + logger, + configurationUtilities + ); + + if (!api[subAction]) { + const errorMessage = `[Action][ExternalService] -> [Swimlane] Unsupported subAction type ${subAction}.`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (!supportedSubActions.includes(subAction)) { + const errorMessage = `[Action][ExternalService] -> [Swimlane] subAction ${subAction} not implemented.`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (subAction === 'createRecord') { + const createRecordParams = subActionParams as ExecutorSubActionCreateRecordParams; + + data = await api.createRecord({ + externalService, + params: createRecordParams, + logger, + }); + + logger.debug(`Swimlane new record id ${data.id}`); + } + + return { status: 'ok', data: data ?? {}, actionId }; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts new file mode 100644 index 0000000000000..8fbbd3edaaa30 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts @@ -0,0 +1,41 @@ +/* + * 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 { + CreateRecordApiParams, + ExecutorSubActionCreateRecordParams, + ExternalService, +} from './types'; + +const createMock = (): jest.Mocked => { + return { + createRecord: jest.fn().mockImplementation(() => + Promise.resolve({ + id: '123456', + }) + ), + }; +}; + +const externalServiceMock = { + create: createMock, +}; + +const executorParams: ExecutorSubActionCreateRecordParams = { + alertName: 'alert-name', + alertSource: 'alert-source', + caseId: 'case-id', + caseName: 'case-name', + comments: 'comments', + severity: 'severity', +}; + +const apiParams: CreateRecordApiParams = { + ...executorParams, +}; + +export { externalServiceMock, executorParams, apiParams }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts new file mode 100644 index 0000000000000..590079c33e780 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +export const ConfigMap = { + id: schema.string(), + key: schema.string(), + name: schema.string(), + fieldType: schema.string(), +}; + +export const ConfigMapSchema = schema.object(ConfigMap); + +export const ConfigMapping = { + alertSourceConfig: ConfigMapSchema, + severityConfig: ConfigMapSchema, + caseNameConfig: schema.nullable(ConfigMapSchema), + caseIdConfig: ConfigMapSchema, + alertNameConfig: ConfigMapSchema, + commentsConfig: schema.nullable(ConfigMapSchema), +}; + +export const ConfigMappingSchema = schema.object(ConfigMapping); + +export const SwimlaneServiceConfiguration = { + apiUrl: schema.string(), + appId: schema.string(), + mappings: ConfigMappingSchema, +}; + +export const SwimlaneServiceConfigurationSchema = schema.object(SwimlaneServiceConfiguration); +// secrets definition + +export const SwimlaneSecretsConfiguration = { + apiToken: schema.string(), +}; + +export const SwimlaneSecretsConfigurationSchema = schema.object(SwimlaneSecretsConfiguration); + +export const ExecutorSubActionSchema = schema.oneOf([ + schema.literal('application'), + schema.literal('createRecord'), +]); + +export const ExecutorSubActionCreateRecordParamsSchema = schema.object({ + alertName: schema.nullable(schema.string()), + severity: schema.nullable(schema.string()), + alertSource: schema.nullable(schema.string()), + caseName: schema.nullable(schema.string()), + caseId: schema.nullable(schema.string()), + comments: schema.nullable(schema.string()), +}); + +export const ExecutorSubActionGetApplicationParamsSchema = schema.object({ id: schema.string() }); + +export const ExecutorParamsSchema = schema.oneOf([ + schema.object({ + subAction: schema.literal('createRecord'), + subActionParams: ExecutorSubActionCreateRecordParamsSchema, + }), +]); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts new file mode 100644 index 0000000000000..e4b8a0a90f11f --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts @@ -0,0 +1,143 @@ +/* + * 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 axios from 'axios'; + +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { Logger } from '../../../../../../src/core/server'; +import { actionsConfigMock } from '../../actions_config.mock'; +import { createExternalService } from './service'; + +const logger = loggingSystemMock.create().get() as jest.Mocked; + +jest.mock('axios'); +jest.mock('../lib/axios_utils', () => { + const originalUtils = jest.requireActual('../lib/axios_utils'); + return { + ...originalUtils, + request: jest.fn(), + }; +}); + +axios.create = jest.fn(() => axios); +const configurationUtilities = actionsConfigMock.create(); + +const mappings = { + alertSourceConfig: { + id: 'adnjls', + name: 'Alert Source', + key: 'alert-source', + fieldType: 'text', + }, + severityConfig: { + id: 'adnlas', + name: 'Severity', + key: 'severity', + fieldType: 'text', + }, + alertNameConfig: { + id: 'adnfls', + name: 'Alert Name', + key: 'alert-name', + fieldType: 'text', + }, + caseIdConfig: { + id: 'a6sst', + name: 'Case Id', + key: 'case-id-name', + fieldType: 'text', + }, + caseNameConfig: { + id: 'a6fst', + name: 'Case Name', + key: 'case-name', + fieldType: 'text', + }, + commentsConfig: { + id: 'a6fdf', + name: 'Comments', + key: 'comments', + fieldType: 'text', + }, +}; + +describe('Swimlane Service', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('createExternalService', () => { + test('throws without url', () => { + expect(() => + createExternalService( + { + config: { + // @ts-ignore + apiUrl: null, + appId: '99999', + mappings, + }, + secrets: { apiToken: '121212' }, + }, + logger, + configurationUtilities + ) + ).toThrow(); + }); + + test('throws without app id', () => { + expect(() => + createExternalService( + { + config: { + apiUrl: 'test.com', + // @ts-ignore + appId: null, + }, + secrets: { apiToken: 'token' }, + }, + logger, + configurationUtilities + ) + ).toThrow(); + }); + + test('throws without mappings', () => { + expect(() => + createExternalService( + { + config: { + apiUrl: 'test.com', + appId: '987987', + // @ts-ignore + mappings: null, + }, + secrets: { apiToken: 'token' }, + }, + logger, + configurationUtilities + ) + ).toThrow(); + }); + + test('throws without api token', () => { + expect(() => { + return createExternalService( + { + config: { apiUrl: 'test.com', appId: '78978', mappings }, + secrets: { + // @ts-ignore + apiToken: null, + }, + }, + logger, + configurationUtilities + ); + }).toThrow(); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts new file mode 100644 index 0000000000000..3d22a2b2064cd --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -0,0 +1,84 @@ +/* + * 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 { Logger } from '@kbn/logging'; +import axios from 'axios'; +import https from 'https'; + +import { + SwimlanePublicConfigurationType, + ExternalService, + ExternalServiceCredentials, + CreateRecordParams, + CreateRecordResponse, + SwimlaneSecretConfigurationType, + MappingConfigType, +} from './types'; +import * as i18n from './translations'; +import { getErrorMessage, request } from '../lib/axios_utils'; +// import { ActionsConfigurationUtilities } from '../../actions_config'; +import { getBodyForEventAction } from './helpers'; +import { ActionsConfigurationUtilities } from '../../actions_config'; + +export const createExternalService = ( + { config, secrets }: ExternalServiceCredentials, + logger: Logger, + configurationUtilities: ActionsConfigurationUtilities +): ExternalService => { + const { apiUrl: url, appId, mappings } = config as SwimlanePublicConfigurationType; + const { apiToken } = secrets as SwimlaneSecretConfigurationType; + + const axiosInstance = axios.create({ + httpsAgent: new https.Agent({ + rejectUnauthorized: false, + }), + }); + + if (!url || !appId || !apiToken || !mappings) { + throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); + } + + const headers: Record = { + 'Content-Type': 'application/json', + 'Private-Token': `${secrets.apiToken}`, + }; + + const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url; + const apiUrl = urlWithoutTrailingSlash.endsWith('api') + ? urlWithoutTrailingSlash + : urlWithoutTrailingSlash + '/api'; + const recordUrl = `${apiUrl}/app/{appId}/record`; + const getPostRecordUrl = (id: string) => recordUrl.replace('{appId}', id); + + const createRecord = async (params: CreateRecordParams): Promise => { + try { + const mappingConfig = mappings as MappingConfigType; + const data = getBodyForEventAction(appId, mappingConfig, params); + const res = await request({ + axios: axiosInstance, + url: getPostRecordUrl(appId), + logger, + configurationUtilities, + headers, + method: 'post', + data, + }); + return { id: res.data.id }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to create record in application with id ${appId}. Error: ${error.message}` + ) + ); + } + }; + + return { + createRecord, + }; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/translations.ts new file mode 100644 index 0000000000000..671cf224448f6 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/translations.ts @@ -0,0 +1,20 @@ +/* + * 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 NAME = i18n.translate('xpack.actions.builtin.case.swimlaneTitle', { + defaultMessage: 'Swimlane', +}); + +export const ALLOWED_HOSTS_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.swimlane.configuration.apiAllowedHostsError', { + defaultMessage: 'error configuring connector action: {message}', + values: { + message, + }, + }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts new file mode 100644 index 0000000000000..76673a992e549 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts @@ -0,0 +1,91 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { TypeOf } from '@kbn/config-schema'; +import { Logger } from '@kbn/logging'; +import { + SwimlaneSecretsConfigurationSchema, + SwimlaneServiceConfigurationSchema, + ExecutorParamsSchema, + ExecutorSubActionCreateRecordParamsSchema, + ExecutorSubActionGetApplicationParamsSchema, + ConfigMappingSchema, +} from './schema'; +import { ActionsConfigurationUtilities } from '../../actions_config'; + +export type SwimlanePublicConfigurationType = TypeOf; +export type SwimlaneSecretConfigurationType = TypeOf; + +export type MappingConfigType = TypeOf & + Record; + +export type ExecutorParams = TypeOf; + +export type ExecutorSubActionCreateRecordParams = TypeOf< + typeof ExecutorSubActionCreateRecordParamsSchema +>; + +export interface ExternalServiceCredentials { + config: SwimlanePublicConfigurationType; + secrets: SwimlaneSecretConfigurationType; +} + +export interface ExternalServiceValidation { + config: (configurationUtilities: ActionsConfigurationUtilities, configObject: any) => void; + secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void; +} + +export type CreateRecordParams = TypeOf & + Record; + +export interface CreateRecordResponse { + id: string; +} + +export interface FieldConfig { + id: string; + name: string; + key: string; + fieldType: string; +} + +export interface ExternalService { + createRecord: (params: CreateRecordParams) => Promise; +} + +export type CreateRecordApiParams = ExecutorSubActionCreateRecordParams; + +export type ExecutorSubActionGetApplicationParams = TypeOf< + typeof ExecutorSubActionGetApplicationParamsSchema +>; + +export interface ExternalServiceApiHandlerArgs { + externalService: ExternalService; +} + +export interface CreateRecordApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: CreateRecordApiParams; + externalService: ExternalService; + logger: Logger; +} + +export interface GetApplicationHandlerArgs { + externalService: ExternalService; +} + +export interface ExternalServiceApi { + createRecord: (args: CreateRecordApiHandlerArgs) => Promise; +} + +export type SwimlaneExecutorResultData = CreateRecordResponse; + +export interface SwimlaneRecordPayload { + applicationId: string; + values?: Record; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/validators.ts new file mode 100644 index 0000000000000..1972cd7e6af0b --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/validators.ts @@ -0,0 +1,28 @@ +/* + * 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 { ActionsConfigurationUtilities } from '../../actions_config'; +import { ExternalServiceValidation, SwimlanePublicConfigurationType } from './types'; +import * as i18n from './translations'; + +export const validateCommonConfig = ( + configurationUtilities: ActionsConfigurationUtilities, + configObject: SwimlanePublicConfigurationType +) => { + try { + configurationUtilities.ensureUriAllowed(configObject.apiUrl); + } catch (allowedListError) { + return i18n.ALLOWED_HOSTS_ERROR(allowedListError.message); + } +}; + +export const validateCommonSecrets = () => {}; + +export const validate: ExternalServiceValidation = { + config: validateCommonConfig, + secrets: validateCommonSecrets, +}; diff --git a/x-pack/plugins/actions/server/usage/actions_usage_collector.ts b/x-pack/plugins/actions/server/usage/actions_usage_collector.ts index f8a91e3a0a67a..3e55ed087a7fe 100644 --- a/x-pack/plugins/actions/server/usage/actions_usage_collector.ts +++ b/x-pack/plugins/actions/server/usage/actions_usage_collector.ts @@ -17,6 +17,7 @@ const byTypeSchema: MakeSchemaFrom['count_by_type'] = { __email: { type: 'long' }, __index: { type: 'long' }, __pagerduty: { type: 'long' }, + __swimlane: { type: 'long' }, '__server-log': { type: 'long' }, __slack: { type: 'long' }, __webhook: { type: 'long' }, diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 47606983b8368..f4f41f96f747c 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -178,6 +178,7 @@ export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ '.email', '.slack', '.pagerduty', + '.swimlane', '.webhook', '.servicenow', '.jira', diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index bb9356014e7a3..48fb82276dba3 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -19,6 +19,9 @@ "__index": { "type": "long" }, + "__swimlane": { + "type": "long" + }, "__pagerduty": { "type": "long" }, @@ -53,6 +56,9 @@ "__index": { "type": "long" }, + "__swimlane": { + "type": "long" + }, "__pagerduty": { "type": "long" }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts index 2eda435d045a4..4266822bda1fc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts @@ -10,6 +10,7 @@ import { getSlackActionType } from './slack'; import { getEmailActionType } from './email'; import { getIndexActionType } from './es_index'; import { getPagerDutyActionType } from './pagerduty'; +import { getSwimlaneActionType } from './swimlane'; import { getWebhookActionType } from './webhook'; import { TypeRegistry } from '../../type_registry'; import { ActionTypeModel } from '../../../types'; @@ -28,6 +29,7 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getEmailActionType()); actionTypeRegistry.register(getIndexActionType()); actionTypeRegistry.register(getPagerDutyActionType()); + actionTypeRegistry.register(getSwimlaneActionType()); actionTypeRegistry.register(getWebhookActionType()); actionTypeRegistry.register(getServiceNowITSMActionType()); actionTypeRegistry.register(getServiceNowSIRActionType()); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts new file mode 100644 index 0000000000000..c0e8c1d9d00d9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts @@ -0,0 +1,40 @@ +/* + * 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 { HttpSetup } from 'kibana/public'; + +export async function getApplication({ + http, + url, + appId, + apiToken, +}: { + http: HttpSetup; + url: string; + appId: string; + apiToken: string; +}): Promise> { + const headers: Record = { + 'Content-Type': 'application/json', + 'Private-Token': `${apiToken}`, + }; + + const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url; + const apiUrl = urlWithoutTrailingSlash.endsWith('api') + ? urlWithoutTrailingSlash + : urlWithoutTrailingSlash + '/api'; + const applicationUrl = `${apiUrl}/app/{appId}`; + + const getApplicationUrl = (id: string) => applicationUrl.replace('{appId}', id); + try { + return await http.get(getApplicationUrl(appId), { + headers, + }); + } catch (error) { + throw new Error(`Unable to get application with id ${appId}. Error: ${error.message}`); + } +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/index.ts new file mode 100644 index 0000000000000..39a57e1bccb61 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { getActionType as getSwimlaneActionType } from './swimlane'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/index.ts new file mode 100644 index 0000000000000..9c8ee334f2143 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/index.ts @@ -0,0 +1,23 @@ +/* + * 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 { SwimlaneConfig, SwimlaneFieldMappingConfig, SwimlaneSecrets } from '../types'; +import { IErrorObject, UserConfiguredActionConnector } from '../../../../../types'; + +export interface StepProps { + action: UserConfiguredActionConnector; + editActionConfig: (property: string, value: any) => void; + editActionSecrets: (property: string, value: any) => void; + errors: IErrorObject; + readOnly: boolean; + updateCurrentStep: (step: number) => void; + updateFields: (items: SwimlaneFieldMappingConfig[]) => void; + fields: SwimlaneFieldMappingConfig[]; +} + +export { SwimlaneConnection } from './swimlane_connection'; +export { SwimlaneFields } from './swimlane_fields'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx new file mode 100644 index 0000000000000..680097054485c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx @@ -0,0 +1,164 @@ +/* + * 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 React, { Fragment } from 'react'; +import { + EuiButton, + EuiCallOut, + EuiFieldText, + EuiFormRow, + EuiLink, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import * as i18n from '../translations'; +import { StepProps } from './'; +import { useKibana } from '../../../../../common/lib/kibana'; +import { getApplication } from '../api'; + +export const SwimlaneConnection: React.FunctionComponent = ({ + action, + editActionConfig, + editActionSecrets, + errors, + readOnly, + updateCurrentStep, + updateFields, +}) => { + const { http } = useKibana().services; + const { apiUrl, appId } = action.config; + const { apiToken } = action.secrets; + const { docLinks } = useKibana().services; + + const isValid = () => { + return apiUrl && apiToken && appId; + }; + + const connectSwimlane = async () => { + // fetch swimlane application configuration + const application = await getApplication({ http, url: apiUrl, appId, apiToken }); + + if (!application) { + throw new Error(i18n.SW_GET_APPLICATION_API_ERROR(appId)); + } + const allFields = application.fields; + updateFields(allFields); + updateCurrentStep(2); + }; + + const getEncryptedFieldNotifyLabel = (isCreate: boolean) => { + if (isCreate) { + return ( + + + + {i18n.SW_REMEMBER_VALUE_LABEL} + + + + ); + } + return ( + + + + + + ); + }; + + const connectSwimlaneButton = ( + + {i18n.SW_RETRIEVE_CONFIGURATION_LABEL} + + ); + + return ( + + + ) => { + editActionConfig('apiUrl', e.target.value); + }} + onBlur={() => { + if (!apiUrl) { + editActionConfig('apiUrl', ''); + } + }} + /> + + + ) => { + editActionConfig('appId', e.target.value); + }} + onBlur={() => { + if (!appId) { + editActionConfig('appId', ''); + } + }} + /> + + + + + } + error={errors.apiToken} + isInvalid={errors.apiToken.length > 0 && apiToken !== undefined} + label={i18n.SW_API_TOKEN_TEXT_FIELD_LABEL} + > + + {getEncryptedFieldNotifyLabel(!action.id)} + 0 && apiToken !== undefined} + // name="apiToken" + readOnly={readOnly} + value={apiToken || ''} + data-test-subj="swimlaneApiTokenInput" + onChange={(e: React.ChangeEvent) => { + editActionSecrets('apiToken', e.target.value); + }} + onBlur={() => { + if (!apiToken) { + editActionSecrets('apiToken', ''); + } + }} + /> + + + + {connectSwimlaneButton} + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx new file mode 100644 index 0000000000000..1522fc96bcacd --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -0,0 +1,146 @@ +/* + * 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 React, { Fragment } from 'react'; +import { EuiButton, EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import * as i18n from '../translations'; +import { StepProps } from './'; + +const SINGLE_SELECTION = { asPlainText: true }; + +export const SwimlaneFields: React.FunctionComponent = ({ + action, + editActionConfig, + updateCurrentStep, + fields, +}) => { + const { mappings } = action.config; + + const options = fields + .filter((f) => f.fieldType === 'text') + .map((f) => ({ label: `${f.name} (${f.key})`, value: f.id })) + .sort((a, b) => (a.label?.toLowerCase() > b.label?.toLowerCase() ? 1 : -1)); + + const findOption = (searchValue: string) => { + return options.find((f) => searchValue === f.value); + }; + + const findItem = (searchValue: string) => { + return fields.find((f) => searchValue === f.id); + }; + + const state = { + alertSourceConfig: findOption(mappings?.alertSourceConfig?.id), + severityConfig: findOption(mappings?.severityConfig?.id), + alertNameConfig: findOption(mappings?.alertNameConfig?.id), + caseIdConfig: findOption(mappings?.caseIdConfig?.id), + caseNameConfig: findOption(mappings?.caseNameConfig?.id), + commentsConfig: findOption(mappings?.commentsConfig?.id), + }; + + const resetConnection = () => { + // reset fields + // setConnectionStatus('incomplete'); + updateCurrentStep(1); + }; + + const editMappings = (key: string, option: EuiComboBoxOptionOption) => { + if (!option?.value) { + return; + } + const item = findItem(option.value); + if (!item) { + return; + } + const newProps = { + ...mappings, + [key]: { id: item.id, name: item.name, key: item.key, fieldType: item.fieldType }, + }; + editActionConfig('mappings', newProps); + }; + + const empty = { label: '', value: '' }; + return ( + + + { + editMappings('alertSourceConfig', e[0]); + }} + /> + + + { + editMappings('severityConfig', e[0]); + }} + /> + + + { + editMappings('alertNameConfig', e[0]); + }} + /> + + + { + editMappings('caseIdConfig', e[0]); + }} + /> + + + { + editMappings('caseNameConfig', e[0]); + }} + /> + + + { + editMappings('commentsConfig', e[0]); + }} + /> + + + {i18n.SW_RETRIEVE_CONFIGURATION_RESET_LABEL} + + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.svg b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.svg new file mode 100644 index 0000000000000..b683e68a8ad87 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx new file mode 100644 index 0000000000000..ddfa5b23234a7 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx @@ -0,0 +1,116 @@ +/* + * 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 { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '.././index'; +import { ActionTypeModel } from '../../../../types'; +import { SwimlaneActionConnector } from './types'; + +const ACTION_TYPE_ID = '.swimlane'; +let actionTypeModel: ActionTypeModel; + +beforeAll(() => { + const actionTypeRegistry = new TypeRegistry(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + expect(actionTypeModel.iconClass).toEqual('test-file-stub'); + }); +}); + +describe('swimlane connector validation', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + mappings: { + caseIdConfig: { id: '1234' }, + }, + }, + } as SwimlaneActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + apiToken: [], + apiUrl: [], + appId: [], + mappings: [], + }, + }); + + // @ts-ignore + delete actionConnector.config.apiUrl; + actionConnector.secrets.apiToken = 'test1'; + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + apiToken: [], + apiUrl: [], + appId: [], + mappings: [], + }, + }); + }); + + test('connector validation fails when connector config is not valid', () => { + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + }, + } as SwimlaneActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + apiToken: [], + apiUrl: [], + appId: [], + mappings: ['Field mappings are required.'], + }, + }); + }); +}); + +describe('swimlane action params validation', () => { + test('action params validation succeeds when action params is valid', () => { + const actionParams = { + subActionParams: { + alertName: 'Alert Name', + }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + alertName: [], + alertSource: [], + caseId: [], + caseName: [], + comments: [], + severity: [], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx new file mode 100644 index 0000000000000..3e5b20c3d184a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { lazy } from 'react'; +import { + ActionTypeModel, + ConnectorValidationResult, + GenericValidationResult, +} from '../../../../types'; +import { + SwimlaneActionConnector, + SwimlaneConfig, + SwimlaneSecrets, + SwimlaneActionParams, +} from './types'; +import swimlaneSvg from './swimlane.svg'; +import * as i18n from './translations'; +import { isValidUrl } from '../../../lib/value_validators'; + +export function getActionType(): ActionTypeModel< + SwimlaneConfig, + SwimlaneSecrets, + SwimlaneActionParams +> { + return { + id: '.swimlane', + iconClass: swimlaneSvg, + selectMessage: i18n.SW_SELECT_MESSAGE_TEXT, + actionTypeTitle: i18n.SW_ACTION_TYPE_TITLE, + validateConnector: ( + action: SwimlaneActionConnector + ): ConnectorValidationResult => { + const configErrors = { + apiUrl: new Array(), + appId: new Array(), + mappings: new Array(), + }; + const secretsErrors = { + apiToken: new Array(), + }; + + const validationResult = { + config: { errors: configErrors }, + secrets: { errors: secretsErrors }, + }; + + if (!action.config.apiUrl) { + configErrors.apiUrl = [...configErrors.apiUrl, i18n.SW_API_URL_REQUIRED]; + } else if (action.config.apiUrl) { + if (!isValidUrl(action.config.apiUrl)) { + configErrors.apiUrl = [...configErrors.apiUrl, i18n.SW_API_URL_INVALID]; + } + } + + if (!action.secrets.apiToken) { + secretsErrors.apiToken = [...secretsErrors.apiToken, i18n.SW_REQUIRED_API_TOKEN_TEXT]; + } + if (!action.config.appId) { + configErrors.appId = [...configErrors.appId, i18n.SW_REQUIRED_APP_ID_TEXT]; + } + if (!action.config.mappings) { + configErrors.mappings = [...configErrors.mappings, i18n.SW_REQUIRED_FIELD_MAPPINGS_TEXT]; + } + return validationResult; + }, + validateParams: (actionParams: SwimlaneActionParams): GenericValidationResult => { + const validationResult = { errors: {} }; + const errors = { + alertName: new Array(), + caseId: new Array(), + severity: new Array(), + caseName: new Array(), + alertSource: new Array(), + comments: new Array(), + }; + validationResult.errors = errors; + if (!actionParams.subActionParams || !actionParams.subActionParams.alertName?.length) { + errors.alertName.push(i18n.SW_REQUIRED_ALERT_NAME); + } + return validationResult; + }, + actionConnectorFields: lazy(() => import('./swimlane_connectors')), + actionParamsFields: lazy(() => import('./swimlane_params')), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx new file mode 100644 index 0000000000000..1ffea54606e75 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx @@ -0,0 +1,111 @@ +/* + * 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 React from 'react'; +import { mountWithIntl, nextTick } from '@kbn/test/jest'; +import { act } from 'react-dom/test-utils'; +import { SwimlaneActionConnector } from './types'; +import SwimlaneActionConnectorFields from './swimlane_connectors'; +jest.mock('../../../../common/lib/kibana'); + +describe('SwimlaneActionConnectorFields renders', () => { + test('all connector fields is rendered', async () => { + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + mappings: { + alertSourceConfig: { id: '123', key: 'product-source' }, + severityConfig: { id: '123', key: 'severity' }, + caseNameConfig: { id: '123', key: 'case-name' }, + caseIdConfig: { id: '123', key: 'case-id' }, + }, + }, + } as SwimlaneActionConnector; + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="swimlaneApiUrlInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneApiUrlInput"]').first().prop('value')).toBe( + 'http:\\test' + ); + expect(wrapper.find('[data-test-subj="swimlaneAppIdInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneApiTokenInput"]').length > 0).toBeTruthy(); + }); + + test('should display a message on create to remember credentials', () => { + const actionConnector = { + actionTypeId: '.swimlane', + secrets: {}, + config: {}, + } as SwimlaneActionConnector; + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); + }); + + test('should display a message on edit to re-enter credentials', () => { + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + mappings: { + alertSourceConfig: { id: '123', key: 'product-source' }, + severityConfig: { id: '123', key: 'severity' }, + caseNameConfig: { id: '123', key: 'case-name' }, + caseIdConfig: { id: '123', key: 'case-id' }, + }, + }, + } as SwimlaneActionConnector; + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx new file mode 100644 index 0000000000000..3918406d8b7a1 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx @@ -0,0 +1,72 @@ +/* + * 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 React, { Fragment, useState } from 'react'; +import { EuiForm, EuiSpacer, EuiStepsHorizontal, EuiStepStatus } from '@elastic/eui'; +import * as i18n from './translations'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import { SwimlaneActionConnector, SwimlaneFieldMappingConfig } from './types'; +import { SwimlaneConnection, SwimlaneFields } from './steps'; + +const SwimlaneActionConnectorFields: React.FunctionComponent< + ActionConnectorFieldsProps +> = ({ errors, action, editActionConfig, editActionSecrets, readOnly }) => { + const [currentStep, setCurrentStep] = useState(1); + const stepMap: { [key: number]: any } = { + 1: SwimlaneConnection, + 2: SwimlaneFields, + }; + const CurrentStepForm = stepMap[currentStep]; + const [connectionStatus] = useState('incomplete' as EuiStepStatus); + const [fieldsConfigured] = useState('incomplete' as EuiStepStatus); + + const [fields, setFields] = useState(new Array()); + + const updateCurrentStep = (step: number) => { + setCurrentStep(step); + }; + + const updateFields = (items: SwimlaneFieldMappingConfig[]) => { + setFields(items); + }; + + const setupSteps = [ + { + title: i18n.SW_CONFIGURE_CONNECTION_LABEL, + status: connectionStatus, + onClick: () => {}, + }, + { + title: i18n.SW_MAPPING_TITLE_TEXT_FIELD_LABEL, + disabled: connectionStatus !== 'complete', + status: fieldsConfigured, + onClick: () => {}, + }, + ]; + + return ( + + + + + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SwimlaneActionConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx new file mode 100644 index 0000000000000..2b61a9c5aa34f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import SwimlaneParamsFields from './swimlane_params'; + +describe('SwimlaneParamsFields renders', () => { + test('all params fields is rendered', () => { + const actionParams = { + subActionParams: { + alertName: 'alert name', + alertSource: 'alert source', + caseId: '3456789', + caseName: 'my case name', + comments: 'my comments', + severity: 'critical', + }, + }; + + const wrapper = mountWithIntl( + {}} + index={0} + /> + ); + expect(wrapper.find('[data-test-subj="severity"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="caseId"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="caseName"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="comments"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="alertSource"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="alertName"]').length > 0).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx new file mode 100644 index 0000000000000..aa9da6e96fd8b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx @@ -0,0 +1,155 @@ +/* + * 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 React, { useCallback, useEffect, useRef } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import * as i18n from './translations'; +import { ActionParamsProps } from '../../../../types'; +import { SwimlaneActionParams } from './types'; +import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; +import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; + +const SwimlaneParamsFields: React.FunctionComponent> = ({ + actionParams, + editAction, + index, + messageVariables, + errors, +}) => { + const isInit = useRef(true); + const { + subActionParams = { + alertName: '', + severity: '', + alertSource: '', + caseName: '', + caseId: '', + comments: '', + }, + } = actionParams; + + const editSubActionProperty = useCallback( + (key: string, value: any) => { + const newProps = { ...subActionParams, [key]: value }; + editAction('subActionParams', newProps, index); + }, + [subActionParams, editAction, index] + ); + + useEffect(() => { + if (isInit.current) { + isInit.current = false; + editAction('subAction', 'createRecord', index); + } + }, [index, editAction]); + + return ( + <> + 0 && subActionParams?.alertName !== undefined} + label={i18n.SW_ALERT_NAME_FIELD_LABEL} + > + + + + 0 && subActionParams?.alertSource !== undefined} + label={i18n.SW_ALERT_SOURCE_FIELD_LABEL} + > + + + + + + + + + + + + 0 && subActionParams?.caseId !== undefined} + label={i18n.SW_CASE_ID_FIELD_LABEL} + > + + + + 0 && subActionParams?.caseName !== undefined} + label={i18n.SW_CASE_NAME_FIELD_LABEL} + > + + + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SwimlaneParamsFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts new file mode 100644 index 0000000000000..43f3f264c651e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts @@ -0,0 +1,175 @@ +/* + * 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 SW_SELECT_MESSAGE_TEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.selectMessageText', + { + defaultMessage: 'Create record in Swimlane', + } +); + +export const SW_ACTION_TYPE_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.actionTypeTitle', + { + defaultMessage: 'Create Swimlane Record', + } +); + +export const SW_REQUIRED_ALERT_NAME = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertName', + { + defaultMessage: 'AlertName is required.', + } +); + +export const SW_REQUIRED_APP_ID_TEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAppIdText', + { + defaultMessage: 'An AppId is required.', + } +); + +export const SW_REQUIRED_FIELD_MAPPINGS_TEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAppIdText', + { + defaultMessage: 'Field mappings are required.', + } +); + +export const SW_REQUIRED_API_TOKEN_TEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredApiTokenText', + { + defaultMessage: 'An API token is required.', + } +); + +export const SW_GET_APPLICATION_API_ERROR = (id: string | null) => + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlane.unableToGetApplicationMessage', + { + defaultMessage: 'Unable to get application with id {id}', + values: { id }, + } + ); + +export const SW_API_URL_TEXT_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiUrlTextFieldLabel', + { + defaultMessage: 'API URL', + } +); + +export const SW_API_URL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.requiredApiUrlTextField', + { + defaultMessage: 'URL is required.', + } +); + +export const SW_API_URL_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.invalidApiUrlTextField', + { + defaultMessage: 'URL is invalid.', + } +); + +export const SW_APP_ID_TEXT_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.appIdTextFieldLabel', + { + defaultMessage: 'Application Id', + } +); + +export const SW_API_TOKEN_TEXT_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiTokenTextFieldLabel', + { + defaultMessage: 'API Token', + } +); + +export const SW_MAPPING_TITLE_TEXT_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingTitleTextFieldLabel', + { + defaultMessage: 'Configure Field Mappings', + } +); + +export const SW_ALERT_SOURCE_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertSourceFieldLabel', + { + defaultMessage: 'Source', + } +); + +export const SW_SEVERITY_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.severityFieldLabel', + { + defaultMessage: 'Severity', + } +); + +export const SW_MAPPING_DESCRIPTION_TEXT_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingDescriptionTextFieldLabel', + { + defaultMessage: 'Used to specify the field names in the Swimlane Application', + } +); + +export const SW_ALERT_NAME_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertNameFieldLabel', + { + defaultMessage: 'Alert Name', + } +); + +export const SW_CASE_ID_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.caseIdFieldLabel', + { + defaultMessage: 'Case ID', + } +); + +export const SW_CASE_NAME_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.caseNameFieldLabel', + { + defaultMessage: 'Case Name', + } +); + +export const SW_COMMENTS_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.commentsFieldLabel', + { + defaultMessage: 'Comments', + } +); + +export const SW_REMEMBER_VALUE_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.rememberValueLabel', + { defaultMessage: 'Remember this value. You must reenter it each time you edit the connector.' } +); + +export const SW_REENTER_VALUE_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.reenterValueLabel', + { defaultMessage: 'This key is encrypted. Please reenter a value for this field.' } +); + +export const SW_CONFIGURE_CONNECTION_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.configureConnectionLabel', + { defaultMessage: 'Configure API Connection' } +); + +export const SW_RETRIEVE_CONFIGURATION_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.retrieveConfigurationLabel', + { defaultMessage: 'Configure Fields' } +); + +export const SW_RETRIEVE_CONFIGURATION_RESET_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.resetConfigurationLabel', + { defaultMessage: 'Reset Configuration' } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts new file mode 100644 index 0000000000000..184abe358a8d2 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UserConfiguredActionConnector } from '../../../../types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ExecutorSubActionCreateRecordParams } from '../../../../../../actions/server/builtin_action_types/swimlane/types'; + +export type SwimlaneActionConnector = UserConfiguredActionConnector< + SwimlaneConfig, + SwimlaneSecrets +>; + +export interface SwimlaneConfig { + apiUrl: string; + appId: string; + mappings: SwimlaneMappingConfig; +} + +export interface SwimlaneMappingConfig { + alertSourceConfig: SwimlaneFieldMappingConfig; + severityConfig: SwimlaneFieldMappingConfig; + caseNameConfig: SwimlaneFieldMappingConfig; + caseIdConfig: SwimlaneFieldMappingConfig; + alertNameConfig: SwimlaneFieldMappingConfig; + commentsConfig: SwimlaneFieldMappingConfig; +} + +export interface SwimlaneFieldMappingConfig { + id: string; + key: string; + name: string; + fieldType: string; +} + +export interface SwimlaneSecrets { + apiToken: string; +} + +export interface SwimlaneActionParams { + subAction: string; + subActionParams: ExecutorSubActionCreateRecordParams; +} + +export interface SwimlaneFieldMap { + key: string; + name: string; +} diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts new file mode 100644 index 0000000000000..8bc8c20d9a195 --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts @@ -0,0 +1,39 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function swimlaneTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('swimlane action', () => { + it('should return 403 when creating a swimlane action', async () => { + await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane action', + actionTypeId: '.swimlane', + config: { + apiUrl: 'http://localhost', + appId: '123456asdf', + username: 'username', + }, + secrets: { + apiToken: 'swimlane-api-key', + }, + }) + .expect(403, { + statusCode: 403, + error: 'Forbidden', + message: + 'Action type .swimlane is disabled because your basic license does not support it. Please upgrade your license.', + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts index 3f0524750d5f8..21cb0db3057bb 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts @@ -14,6 +14,7 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./builtin_action_types/es_index')); loadTestFile(require.resolve('./builtin_action_types/jira')); loadTestFile(require.resolve('./builtin_action_types/pagerduty')); + loadTestFile(require.resolve('./builtin_action_types/swimlane')); loadTestFile(require.resolve('./builtin_action_types/server_log')); loadTestFile(require.resolve('./builtin_action_types/servicenow')); loadTestFile(require.resolve('./builtin_action_types/slack')); diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 560ff6c0b317f..15a19277a250a 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -27,6 +27,7 @@ const enabledActionTypes = [ '.email', '.index', '.pagerduty', + '.swimlane', '.server-log', '.servicenow', '.jira', diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/README.md b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/README.md index c248bdce3785d..26af24e73c7c1 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/README.md +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/README.md @@ -17,7 +17,7 @@ simulator usage This may get out of date, consult the code for exact urls and inputs. Each simulator's last path segment should be the name of the service (eg, slack, -pagerduty, etc). +pagerduty, swimlane, etc). ```console $ export SLACK_URL=http://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/slack diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts index 878507bcf4afc..a1fb02285ef5c 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts @@ -13,6 +13,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../.. import { PluginSetupContract as ActionsPluginSetupContract } from '../../../../../../../plugins/actions/server/plugin'; import { ActionType } from '../../../../../../../plugins/actions/server'; import { initPlugin as initPagerduty } from './pagerduty_simulation'; +import { initPlugin as initSwimlane } from './swimlane_simulation'; import { initPlugin as initServiceNow } from './servicenow_simulation'; import { initPlugin as initJira } from './jira_simulation'; import { initPlugin as initResilient } from './resilient_simulation'; @@ -23,6 +24,7 @@ export const NAME = 'actions-FTS-external-service-simulators'; export enum ExternalServiceSimulator { PAGERDUTY = 'pagerduty', + SWIMLANE = 'swimlane', SERVICENOW = 'servicenow', SLACK = 'slack', JIRA = 'jira', @@ -117,6 +119,7 @@ export class FixturePlugin implements Plugin, + res: KibanaResponseFactory + ): Promise> { + const { body } = req; + + return jsonResponse(res, 202, { + status: 'success', + message: 'Event processed', + ...(body.alertName ? { alertName: body.alertName } : {}), + }); + } + ); +} + +function jsonResponse( + res: KibanaResponseFactory, + code: number, + object: Record = {} +) { + return res.custom>({ body: object, statusCode: code }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts new file mode 100644 index 0000000000000..4268f2993a028 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts @@ -0,0 +1,227 @@ +/* + * 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 httpProxy from 'http-proxy'; +import expect from '@kbn/expect'; + +import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { + getExternalServiceSimulatorPath, + ExternalServiceSimulator, +} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; + +// eslint-disable-next-line import/no-default-export +export default function swimlaneTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const configService = getService('config'); + + describe('swimlane action', () => { + let simulatedActionId = ''; + let swimlaneSimulatorURL: string = ''; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; + + // need to wait for kibanaServer to settle ... + before(async () => { + swimlaneSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SWIMLANE) + ); + + proxyServer = await getHttpProxyServer( + kibanaServer.resolveUrl('/'), + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); + }); + + it('should return successfully when passed valid create parameters', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane action', + actionTypeId: '.swimlane', + config: { + apiUrl: swimlaneSimulatorURL, + appId: '123445asdfasdf', + username: 'username', + }, + secrets: { + apiToken: 'swimlane-api-token', + }, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + isPreconfigured: false, + name: 'A swimlane action', + actionTypeId: '.swimlane', + config: { + apiUrl: swimlaneSimulatorURL, + }, + }); + + expect(typeof createdAction.id).to.be('string'); + + const { body: fetchedAction } = await supertest + .get(`/api/actions/action/${createdAction.id}`) + .expect(200); + + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + isPreconfigured: false, + name: 'A swimlane action', + actionTypeId: '.swimlane', + config: { + apiUrl: swimlaneSimulatorURL, + }, + }); + }); + + it('should return unsuccessfully when passed invalid create parameters', async () => { + await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane action', + actionTypeId: '.swimlane', + config: { + apiUrl: swimlaneSimulatorURL, + }, + secrets: {}, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type secrets: [routingKey]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should return unsuccessfully when default swimlane url is not present in allowedHosts', async () => { + await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane action', + actionTypeId: '.swimlane', + secrets: {}, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: error configuring swimlane action: target url "https://events.swimlane.com/v2/enqueue" is not added to the Kibana config xpack.actions.allowedHosts', + }); + }); + }); + + it('should create swimlane simulator action successfully', async () => { + const { body: createdSimulatedAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane simulator', + actionTypeId: '.swimlane', + config: { + apiUrl: swimlaneSimulatorURL, + }, + secrets: { + routingKey: 'pager-duty-routing-key', + }, + }) + .expect(200); + + simulatedActionId = createdSimulatedAction.id; + }); + + it('should handle executing with a simulated success', async () => { + const { body: result } = await supertest + .post(`/api/actions/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + summary: 'just a test', + }, + }) + .expect(200); + + expect(proxyHaveBeenCalled).to.equal(true); + expect(result).to.eql({ + status: 'ok', + actionId: simulatedActionId, + data: { + message: 'Event processed', + status: 'success', + }, + }); + }); + + it('should handle a 40x swimlane error', async () => { + const { body: result } = await supertest + .post(`/api/actions/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + summary: 'respond-with-418', + }, + }) + .expect(200); + expect(result.status).to.equal('error'); + expect(result.message).to.match(/error posting swimlane event: unexpected status 418/); + }); + + it('should handle a 429 swimlane error', async () => { + const { body: result } = await supertest + .post(`/api/actions/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + summary: 'respond-with-429', + }, + }) + .expect(200); + + expect(result.status).to.equal('error'); + expect(result.message).to.match(/error posting swimlane event: http status 429, retry later/); + expect(result.retry).to.equal(true); + }); + + it('should handle a 500 swimlane error', async () => { + const { body: result } = await supertest + .post(`/api/actions/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + summary: 'respond-with-502', + }, + }) + .expect(200); + + expect(result.status).to.equal('error'); + expect(result.message).to.match(/error posting swimlane event: http status 502/); + expect(result.retry).to.equal(true); + }); + + after(() => { + if (proxyServer) { + proxyServer.close(); + } + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts index b5ff287ac58f6..db57af0ba1a98 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts @@ -23,6 +23,7 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo loadTestFile(require.resolve('./builtin_action_types/es_index')); loadTestFile(require.resolve('./builtin_action_types/es_index_preconfigured')); loadTestFile(require.resolve('./builtin_action_types/pagerduty')); + loadTestFile(require.resolve('./builtin_action_types/swimlane')); loadTestFile(require.resolve('./builtin_action_types/server_log')); loadTestFile(require.resolve('./builtin_action_types/servicenow')); loadTestFile(require.resolve('./builtin_action_types/jira')); diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 3c81407276453..5d49987be865f 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -25,6 +25,7 @@ const enabledActionTypes = [ '.index', '.jira', '.pagerduty', + '.swimlane', '.resilient', '.server-log', '.servicenow', diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index 355703ae35052..e93663041ebed 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -20,6 +20,7 @@ const enabledActionTypes = [ '.email', '.index', '.pagerduty', + '.swimlane', '.server-log', '.servicenow', '.slack', diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index 5dd1890e240a4..fc2a6694e5e28 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -16,6 +16,7 @@ const enabledActionTypes = [ '.email', '.index', '.pagerduty', + '.swimlane', '.servicenow', '.slack', '.webhook', From 3bb04dc0aac098e591937592287a411de66a19fb Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 22 Mar 2021 17:10:31 -0400 Subject: [PATCH 02/96] clean up + rm rejectUnauthorized --- .../builtin_action_types/swimlane/service.ts | 14 ++++---------- .../swimlane/steps/swimlane_connection.tsx | 6 ++---- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index 3d22a2b2064cd..bc4d2d27a049a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -7,8 +7,10 @@ import { Logger } from '@kbn/logging'; import axios from 'axios'; -import https from 'https'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { getErrorMessage, request } from '../lib/axios_utils'; +import { getBodyForEventAction } from './helpers'; import { SwimlanePublicConfigurationType, ExternalService, @@ -19,10 +21,6 @@ import { MappingConfigType, } from './types'; import * as i18n from './translations'; -import { getErrorMessage, request } from '../lib/axios_utils'; -// import { ActionsConfigurationUtilities } from '../../actions_config'; -import { getBodyForEventAction } from './helpers'; -import { ActionsConfigurationUtilities } from '../../actions_config'; export const createExternalService = ( { config, secrets }: ExternalServiceCredentials, @@ -32,11 +30,7 @@ export const createExternalService = ( const { apiUrl: url, appId, mappings } = config as SwimlanePublicConfigurationType; const { apiToken } = secrets as SwimlaneSecretConfigurationType; - const axiosInstance = axios.create({ - httpsAgent: new https.Agent({ - rejectUnauthorized: false, - }), - }); + const axiosInstance = axios.create(); if (!url || !appId || !apiToken || !mappings) { throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx index 680097054485c..fe3bacca8476f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx @@ -4,8 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import React, { Fragment } from 'react'; import { EuiButton, EuiCallOut, @@ -15,7 +13,8 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import React, { Fragment } from 'react'; +import { FormattedMessage } from 'react-intl'; import * as i18n from '../translations'; import { StepProps } from './'; import { useKibana } from '../../../../../common/lib/kibana'; @@ -142,7 +141,6 @@ export const SwimlaneConnection: React.FunctionComponent = ({ 0 && apiToken !== undefined} - // name="apiToken" readOnly={readOnly} value={apiToken || ''} data-test-subj="swimlaneApiTokenInput" From 9c7ce14bd5373f848944a312b5f8b4b597ea9dd1 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 3 May 2021 09:46:09 -0600 Subject: [PATCH 03/96] wip --- .../builtin_action_types/swimlane/helpers.ts | 26 +++++++++++++++++-- .../builtin_action_types/swimlane/schema.ts | 10 +++---- .../builtin_action_types/swimlane/service.ts | 2 ++ .../builtin_action_types/swimlane/types.ts | 8 +++++- .../swimlane/steps/swimlane_fields.tsx | 2 +- 5 files changed, 39 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts index 7934a59f5031a..fdb454cc17e80 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts @@ -5,7 +5,13 @@ * 2.0. */ -import { CreateRecordParams, MappingConfigType, SwimlaneRecordPayload } from './types'; +import { + CreateRecordParams, + MappingConfigType, + SwimlaneRecordPayload, + SwimlaneDataComments, + SwimlaneDataValues, +} from './types'; export const getBodyForEventAction = ( applicationId: string, @@ -16,7 +22,8 @@ export const getBodyForEventAction = ( applicationId, }; - const values: Record = {}; + const values: SwimlaneDataValues = {}; + const comments: SwimlaneDataComments = {}; for (const mappingsKey in mappingConfig) { if (!Object.hasOwnProperty.call(mappingConfig, mappingsKey)) { @@ -29,12 +36,24 @@ export const getBodyForEventAction = ( continue; } + const createdDate = new Date().toISOString(); const { id, fieldType } = fieldMap; const paramName = mappingsKey.replace('Config', ''); if (params[paramName]) { const value = params[paramName]; if (value) { switch (fieldType) { + case 'comments': { + if (comments[id] != null) { + comments[id] = [ + ...comments[id], + { fieldId: id, message: value, createdDate, isRichText: true }, + ]; + } else { + comments[id] = [{ fieldId: id, message: value, createdDate, isRichText: true }]; + } + break; + } case 'numeric': { values[id] = +value; break; @@ -49,6 +68,9 @@ export const getBodyForEventAction = ( } data.values = values; + if (Object.keys(comments).length) { + data.comments = comments; + } return data; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts index 590079c33e780..1bf37b27ab0d7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -17,12 +17,12 @@ export const ConfigMap = { export const ConfigMapSchema = schema.object(ConfigMap); export const ConfigMapping = { + alertNameConfig: ConfigMapSchema, alertSourceConfig: ConfigMapSchema, - severityConfig: ConfigMapSchema, - caseNameConfig: schema.nullable(ConfigMapSchema), caseIdConfig: ConfigMapSchema, - alertNameConfig: ConfigMapSchema, + caseNameConfig: schema.nullable(ConfigMapSchema), commentsConfig: schema.nullable(ConfigMapSchema), + severityConfig: ConfigMapSchema, }; export const ConfigMappingSchema = schema.object(ConfigMapping); @@ -49,11 +49,11 @@ export const ExecutorSubActionSchema = schema.oneOf([ export const ExecutorSubActionCreateRecordParamsSchema = schema.object({ alertName: schema.nullable(schema.string()), - severity: schema.nullable(schema.string()), alertSource: schema.nullable(schema.string()), - caseName: schema.nullable(schema.string()), caseId: schema.nullable(schema.string()), + caseName: schema.nullable(schema.string()), comments: schema.nullable(schema.string()), + severity: schema.nullable(schema.string()), }); export const ExecutorSubActionGetApplicationParamsSchema = schema.object({ id: schema.string() }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index bc4d2d27a049a..dbede22fede30 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -52,6 +52,7 @@ export const createExternalService = ( try { const mappingConfig = mappings as MappingConfigType; const data = getBodyForEventAction(appId, mappingConfig, params); + console.log('data', JSON.stringify(data)); const res = await request({ axios: axiosInstance, url: getPostRecordUrl(appId), @@ -61,6 +62,7 @@ export const createExternalService = ( method: 'post', data, }); + // console.log('res', res); return { id: res.data.id }; } catch (error) { throw new Error( diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts index 76673a992e549..22f2297497798 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts @@ -84,8 +84,14 @@ export interface ExternalServiceApi { } export type SwimlaneExecutorResultData = CreateRecordResponse; +export type SwimlaneDataValues = Record; +export type SwimlaneDataComments = Record< + string, + Array<{ fieldId: string; message: string | number; createdDate: string; isRichText: boolean }> +>; export interface SwimlaneRecordPayload { applicationId: string; - values?: Record; + values?: SwimlaneDataValues; + comments?: SwimlaneDataComments; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index 1522fc96bcacd..200c8eb4a0d44 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -21,7 +21,7 @@ export const SwimlaneFields: React.FunctionComponent = ({ const { mappings } = action.config; const options = fields - .filter((f) => f.fieldType === 'text') + .filter((f) => f.fieldType === 'text' || f.fieldType === 'comments') .map((f) => ({ label: `${f.name} (${f.key})`, value: f.id })) .sort((a, b) => (a.label?.toLowerCase() > b.label?.toLowerCase() ? 1 : -1)); From 440fa5e416531e457ee29226020e8c81e248adb4 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 3 May 2021 10:31:16 -0600 Subject: [PATCH 04/96] better types --- .../builtin_action_types/swimlane/service.ts | 1 - .../cases/common/api/connectors/index.ts | 3 +- .../cases/common/api/connectors/swimlane.ts | 20 ++++++++++++ x-pack/plugins/cases/common/constants.ts | 15 ++++----- .../public/components/connectors/config.ts | 9 +++--- .../components/connectors/jira/index.ts | 4 +-- .../components/connectors/resilient/index.ts | 4 +-- .../components/connectors/servicenow/index.ts | 10 ++++-- .../containers/use_get_action_license.tsx | 3 +- .../cases/server/connectors/case/schema.ts | 31 +++++++++++++------ .../server/connectors/case/validators.ts | 3 +- .../plugins/cases/server/connectors/index.ts | 10 +++--- .../swimlane/external_service_formatter.ts | 0 .../routes/api/__mocks__/request_responses.ts | 14 ++++----- 14 files changed, 81 insertions(+), 46 deletions(-) create mode 100644 x-pack/plugins/cases/common/api/connectors/swimlane.ts create mode 100644 x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index dbede22fede30..7eb4a1f485426 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -62,7 +62,6 @@ export const createExternalService = ( method: 'post', data, }); - // console.log('res', res); return { id: res.data.id }; } catch (error) { throw new Error( diff --git a/x-pack/plugins/cases/common/api/connectors/index.ts b/x-pack/plugins/cases/common/api/connectors/index.ts index f9b7c8b12c2cd..96c5c52399b0e 100644 --- a/x-pack/plugins/cases/common/api/connectors/index.ts +++ b/x-pack/plugins/cases/common/api/connectors/index.ts @@ -32,10 +32,11 @@ export const ConnectorFieldsRt = rt.union([ export enum ConnectorTypes { jira = '.jira', + none = '.none', resilient = '.resilient', serviceNowITSM = '.servicenow', serviceNowSIR = '.servicenow-sir', - none = '.none', + swimlane = '.swimlane', } const ConnectorJiraTypeFieldsRt = rt.type({ diff --git a/x-pack/plugins/cases/common/api/connectors/swimlane.ts b/x-pack/plugins/cases/common/api/connectors/swimlane.ts new file mode 100644 index 0000000000000..92c07ff17bbc8 --- /dev/null +++ b/x-pack/plugins/cases/common/api/connectors/swimlane.ts @@ -0,0 +1,20 @@ +/* + * 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 * as rt from 'io-ts'; + +// New fields should also be added at: x-pack/plugins/cases/server/connectors/case/schema.ts +export const SwimlaneFieldsRT = rt.type({ + alertName: rt.string, + alertSource: rt.union([rt.string, rt.null]), + caseId: rt.union([rt.string, rt.null]), + caseName: rt.union([rt.string, rt.null]), + comments: rt.union([rt.string, rt.null]), + severity: rt.union([rt.string, rt.null]), +}); + +export type SwimlaneFieldsType = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index f9fae2466a59b..e8fe2271060bd 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -4,6 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { ConnectorTypes } from './api'; + export const DEFAULT_DATE_FORMAT = 'dateFormat'; export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz'; @@ -37,16 +39,11 @@ export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`; export const ACTION_URL = '/api/actions'; export const ACTION_TYPES_URL = '/api/actions/list_action_types'; -export const SERVICENOW_ITSM_ACTION_TYPE_ID = '.servicenow'; -export const SERVICENOW_SIR_ACTION_TYPE_ID = '.servicenow-sir'; -export const JIRA_ACTION_TYPE_ID = '.jira'; -export const RESILIENT_ACTION_TYPE_ID = '.resilient'; - export const SUPPORTED_CONNECTORS = [ - SERVICENOW_ITSM_ACTION_TYPE_ID, - SERVICENOW_SIR_ACTION_TYPE_ID, - JIRA_ACTION_TYPE_ID, - RESILIENT_ACTION_TYPE_ID, + ConnectorTypes.serviceNowITSM, + ConnectorTypes.serviceNowSIR, + ConnectorTypes.jira, + ConnectorTypes.resilient, ]; /** diff --git a/x-pack/plugins/cases/public/components/connectors/config.ts b/x-pack/plugins/cases/public/components/connectors/config.ts index e8d87511c7e17..da9989dd53694 100644 --- a/x-pack/plugins/cases/public/components/connectors/config.ts +++ b/x-pack/plugins/cases/public/components/connectors/config.ts @@ -13,6 +13,7 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../triggers_actions_ui/public/common'; import { ConnectorConfiguration } from './types'; +import { ConnectorTypes } from '../../../common'; const resilient = getResilientActionType(); const serviceNowITSM = getServiceNowITSMActionType(); @@ -20,19 +21,19 @@ const serviceNowSIR = getServiceNowSIRActionType(); const jira = getJiraActionType(); export const connectorsConfiguration: Record = { - '.servicenow': { + [ConnectorTypes.serviceNowITSM]: { name: serviceNowITSM.actionTypeTitle ?? '', logo: serviceNowITSM.iconClass, }, - '.servicenow-sir': { + [ConnectorTypes.serviceNowSIR]: { name: serviceNowSIR.actionTypeTitle ?? '', logo: serviceNowSIR.iconClass, }, - '.jira': { + [ConnectorTypes.jira]: { name: jira.actionTypeTitle ?? '', logo: jira.iconClass, }, - '.resilient': { + [ConnectorTypes.resilient]: { name: resilient.actionTypeTitle ?? '', logo: resilient.iconClass, }, diff --git a/x-pack/plugins/cases/public/components/connectors/jira/index.ts b/x-pack/plugins/cases/public/components/connectors/jira/index.ts index ea408a1bd6664..a78dc99bfa17e 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/jira/index.ts @@ -8,14 +8,14 @@ import { lazy } from 'react'; import { CaseConnector } from '../types'; -import { JiraFieldsType } from '../../../../common'; +import { ConnectorTypes, JiraFieldsType } from '../../../../common'; import * as i18n from './translations'; export * from './types'; export const getCaseConnector = (): CaseConnector => { return { - id: '.jira', + id: ConnectorTypes.jira, fieldsComponent: lazy(() => import('./case_fields')), }; }; diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/index.ts b/x-pack/plugins/cases/public/components/connectors/resilient/index.ts index c8e7ad9a063cb..fe0972b81ef6d 100644 --- a/x-pack/plugins/cases/public/components/connectors/resilient/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/resilient/index.ts @@ -8,14 +8,14 @@ import { lazy } from 'react'; import { CaseConnector } from '../types'; -import { ResilientFieldsType } from '../../../../common'; +import { ConnectorTypes, ResilientFieldsType } from '../../../../common'; import * as i18n from './translations'; export * from './types'; export const getCaseConnector = (): CaseConnector => { return { - id: '.resilient', + id: ConnectorTypes.resilient, fieldsComponent: lazy(() => import('./case_fields')), }; }; diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts index a6f0795fe4d8f..87650cf2975d0 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts @@ -8,19 +8,23 @@ import { lazy } from 'react'; import { CaseConnector } from '../types'; -import { ServiceNowITSMFieldsType, ServiceNowSIRFieldsType } from '../../../../common'; +import { + ConnectorTypes, + ServiceNowITSMFieldsType, + ServiceNowSIRFieldsType, +} from '../../../../common'; import * as i18n from './translations'; export const getServiceNowITSMCaseConnector = (): CaseConnector => { return { - id: '.servicenow', + id: ConnectorTypes.serviceNowITSM, fieldsComponent: lazy(() => import('./servicenow_itsm_case_fields')), }; }; export const getServiceNowSIRCaseConnector = (): CaseConnector => { return { - id: '.servicenow-sir', + id: ConnectorTypes.serviceNowSIR, fieldsComponent: lazy(() => import('./servicenow_sir_case_fields')), }; }; diff --git a/x-pack/plugins/cases/public/containers/use_get_action_license.tsx b/x-pack/plugins/cases/public/containers/use_get_action_license.tsx index 4f28d88c14b25..e4ea6d05011a7 100644 --- a/x-pack/plugins/cases/public/containers/use_get_action_license.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_action_license.tsx @@ -11,6 +11,7 @@ import { useToasts } from '../common/lib/kibana'; import { getActionLicense } from './api'; import * as i18n from './translations'; import { ActionLicense } from './types'; +import { ConnectorTypes } from '../../common'; export interface ActionLicenseState { actionLicense: ActionLicense | null; @@ -24,7 +25,7 @@ export const initialData: ActionLicenseState = { isError: false, }; -const MINIMUM_LICENSE_REQUIRED_CONNECTOR = '.jira'; +const MINIMUM_LICENSE_REQUIRED_CONNECTOR = ConnectorTypes.jira; export const useGetActionLicense = (): ActionLicenseState => { const [actionLicenseState, setActionLicensesState] = useState(initialData); diff --git a/x-pack/plugins/cases/server/connectors/case/schema.ts b/x-pack/plugins/cases/server/connectors/case/schema.ts index 803b01cbbdc57..e016ce6577eee 100644 --- a/x-pack/plugins/cases/server/connectors/case/schema.ts +++ b/x-pack/plugins/cases/server/connectors/case/schema.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { CommentType } from '../../../common'; +import { CommentType, ConnectorTypes } from '../../../common'; import { validateConnector } from './validators'; // Reserved for future implementation @@ -51,6 +51,15 @@ const JiraFieldsSchema = schema.object({ parent: schema.nullable(schema.string()), }); +const SwimlaneFieldsSchema = schema.object({ + alertName: schema.string(), + alertSource: schema.nullable(schema.string()), + caseId: schema.nullable(schema.string()), + caseName: schema.nullable(schema.string()), + comments: schema.nullable(schema.string()), + severity: schema.nullable(schema.string()), +}); + const ResilientFieldsSchema = schema.object({ incidentTypes: schema.nullable(schema.arrayOf(schema.string())), severityCode: schema.nullable(schema.string()), @@ -77,20 +86,22 @@ const ServiceNowSIRFieldsSchema = schema.object({ const NoneFieldsSchema = schema.nullable(schema.object({})); const ReducedConnectorFieldsSchema: { [x: string]: any } = { - '.jira': JiraFieldsSchema, - '.resilient': ResilientFieldsSchema, - '.servicenow-sir': ServiceNowSIRFieldsSchema, + [ConnectorTypes.jira]: JiraFieldsSchema, + [ConnectorTypes.resilient]: ResilientFieldsSchema, + [ConnectorTypes.serviceNowSIR]: ServiceNowSIRFieldsSchema, + [ConnectorTypes.swimlane]: SwimlaneFieldsSchema, }; export const ConnectorProps = { id: schema.string(), name: schema.string(), type: schema.oneOf([ - schema.literal('.servicenow'), - schema.literal('.jira'), - schema.literal('.resilient'), - schema.literal('.servicenow-sir'), - schema.literal('.none'), + schema.literal(ConnectorTypes.jira), + schema.literal(ConnectorTypes.none), + schema.literal(ConnectorTypes.resilient), + schema.literal(ConnectorTypes.serviceNowITSM), + schema.literal(ConnectorTypes.serviceNowSIR), + schema.literal(ConnectorTypes.swimlane), ]), // Chain of conditional schemes fields: Object.keys(ReducedConnectorFieldsSchema).reduce( @@ -103,7 +114,7 @@ export const ConnectorProps = { ), schema.conditional( schema.siblingRef('type'), - '.servicenow', + ConnectorTypes.serviceNowITSM, ServiceNowITSMFieldsSchema, NoneFieldsSchema ) diff --git a/x-pack/plugins/cases/server/connectors/case/validators.ts b/x-pack/plugins/cases/server/connectors/case/validators.ts index 03110d15c9d3f..6ab4f3a21a24f 100644 --- a/x-pack/plugins/cases/server/connectors/case/validators.ts +++ b/x-pack/plugins/cases/server/connectors/case/validators.ts @@ -6,9 +6,10 @@ */ import { Connector } from './types'; +import { ConnectorTypes } from '../../../common'; export const validateConnector = (connector: Connector) => { - if (connector.type === '.none' && connector.fields !== null) { + if (connector.type === ConnectorTypes.none && connector.fields !== null) { return 'Fields must be set to null for connectors of type .none'; } }; diff --git a/x-pack/plugins/cases/server/connectors/index.ts b/x-pack/plugins/cases/server/connectors/index.ts index ecf04e4f7b0f1..d8f5479530827 100644 --- a/x-pack/plugins/cases/server/connectors/index.ts +++ b/x-pack/plugins/cases/server/connectors/index.ts @@ -17,7 +17,7 @@ import { serviceNowITSMExternalServiceFormatter } from './servicenow/itsm_format import { serviceNowSIRExternalServiceFormatter } from './servicenow/sir_formatter'; import { jiraExternalServiceFormatter } from './jira/external_service_formatter'; import { resilientExternalServiceFormatter } from './resilient/external_service_formatter'; -import { CommentRequest, CommentType } from '../../common'; +import { CommentRequest, CommentType, ConnectorTypes } from '../../common'; export * from './types'; export { transformConnectorComment } from './case'; @@ -50,10 +50,10 @@ export const registerConnectors = ({ }; export const externalServiceFormatters: ExternalServiceFormatterMapper = { - '.servicenow': serviceNowITSMExternalServiceFormatter, - '.servicenow-sir': serviceNowSIRExternalServiceFormatter, - '.jira': jiraExternalServiceFormatter, - '.resilient': resilientExternalServiceFormatter, + [ConnectorTypes.serviceNowITSM]: serviceNowITSMExternalServiceFormatter, + [ConnectorTypes.serviceNowSIR]: serviceNowSIRExternalServiceFormatter, + [ConnectorTypes.jira]: jiraExternalServiceFormatter, + [ConnectorTypes.resilient]: resilientExternalServiceFormatter, }; export const isCommentGeneratedAlert = ( diff --git a/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts b/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts index 9df94cd0923c9..8d642e47eabab 100644 --- a/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts +++ b/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts @@ -44,7 +44,7 @@ export const getActions = (): FindActionResult[] => [ }, { id: '123', - actionTypeId: '.servicenow', + actionTypeId: ConnectorTypes.serviceNowITSM, name: 'ServiceNow', config: { apiUrl: 'https://dev102283.service-now.com', @@ -54,7 +54,7 @@ export const getActions = (): FindActionResult[] => [ }, { id: '456', - actionTypeId: '.jira', + actionTypeId: ConnectorTypes.jira, name: 'Connector without isCaseOwned', config: { apiUrl: 'https://elastic.jira.com', @@ -64,7 +64,7 @@ export const getActions = (): FindActionResult[] => [ }, { id: '789', - actionTypeId: '.resilient', + actionTypeId: ConnectorTypes.resilient, name: 'Connector without mapping', config: { apiUrl: 'https://elastic.resilient.com', @@ -74,7 +74,7 @@ export const getActions = (): FindActionResult[] => [ }, { id: 'for-mock-case-id-3', - actionTypeId: '.jira', + actionTypeId: ConnectorTypes.jira, name: 'For mock case id 3', config: { apiUrl: 'https://elastic.jira.com', @@ -102,7 +102,7 @@ export const getActionTypes = (): ActionTypeConnector[] => [ enabledInLicense: true, }, { - id: '.servicenow', + id: ConnectorTypes.serviceNowITSM, name: 'ServiceNow', minimumLicenseRequired: 'platinum', enabled: false, @@ -110,7 +110,7 @@ export const getActionTypes = (): ActionTypeConnector[] => [ enabledInLicense: true, }, { - id: '.jira', + id: ConnectorTypes.jira, name: 'Jira', minimumLicenseRequired: 'gold', enabled: true, @@ -118,7 +118,7 @@ export const getActionTypes = (): ActionTypeConnector[] => [ enabledInLicense: true, }, { - id: '.resilient', + id: ConnectorTypes.resilient, name: 'IBM Resilient', minimumLicenseRequired: 'platinum', enabled: false, From 9d9662f853a0f2e7aa3722f9293b0f9790a8a945 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 3 May 2021 15:14:36 -0600 Subject: [PATCH 05/96] cases maps --- .../builtin_action_types/jira/schema.ts | 8 ----- .../server/builtin_action_types/jira/types.ts | 4 +-- .../builtin_action_types/resilient/schema.ts | 8 ----- .../builtin_action_types/servicenow/schema.ts | 8 ----- .../builtin_action_types/swimlane/schema.ts | 6 +--- .../builtin_action_types/swimlane/types.ts | 9 ++++++ x-pack/plugins/cases/README.md | 2 +- .../cases/common/api/connectors/index.ts | 11 ++++++- .../cases/common/api/connectors/mappings.ts | 9 ++---- x-pack/plugins/cases/common/constants.ts | 1 + .../public/components/connectors/config.ts | 6 ++++ x-pack/plugins/cases/server/client/client.ts | 6 ++-- .../client/configure/get_default_mappings.ts | 32 +++++++++++++++++++ .../client/configure/get_fields.test.ts | 6 +--- .../server/client/configure/get_fields.ts | 32 ------------------- .../server/client/configure/get_mappings.ts | 4 +-- .../cases/server/client/configure/utils.ts | 25 ++++++++++++--- x-pack/plugins/cases/server/client/types.ts | 8 ++--- .../plugins/cases/server/connectors/index.ts | 2 ++ .../swimlane/external_service_formatter.ts | 25 +++++++++++++++ .../routes/api/__mocks__/request_responses.ts | 8 +++++ .../api/cases/configure/get_connectors.ts | 8 +++-- .../public/common/index.ts | 1 + 23 files changed, 137 insertions(+), 92 deletions(-) create mode 100644 x-pack/plugins/cases/server/client/configure/get_default_mappings.ts delete mode 100644 x-pack/plugins/cases/server/client/configure/get_fields.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index a81dfaeef8175..eb2f540deaa9a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -25,14 +25,6 @@ export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( ExternalIncidentServiceSecretConfiguration ); -export const ExecutorSubActionSchema = schema.oneOf([ - schema.literal('getIncident'), - schema.literal('pushToService'), - schema.literal('handshake'), - schema.literal('issueTypes'), - schema.literal('fieldsByIssueType'), -]); - export const ExecutorSubActionPushParamsSchema = schema.object({ incident: schema.object({ summary: schema.string(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index 89a5551554c4a..1296782309d73 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -16,10 +16,10 @@ import { ExecutorSubActionGetIncidentParamsSchema, ExecutorSubActionHandshakeParamsSchema, ExecutorSubActionGetCapabilitiesParamsSchema, - ExecutorSubActionGetIssueTypesParamsSchema, ExecutorSubActionGetFieldsByIssueTypeParamsSchema, ExecutorSubActionGetIssuesParamsSchema, ExecutorSubActionGetIssueParamsSchema, + ExecutorSubActionCommonFieldsParamsSchema, } from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { Logger } from '../../../../../../src/core/server'; @@ -125,7 +125,7 @@ export type ExecutorSubActionGetCapabilitiesParams = TypeOf< >; export type ExecutorSubActionGetIssueTypesParams = TypeOf< - typeof ExecutorSubActionGetIssueTypesParamsSchema + typeof ExecutorSubActionCommonFieldsParamsSchema >; export type ExecutorSubActionGetFieldsByIssueTypeParams = TypeOf< diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts index 9095780fea17c..9f76a236cacd5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts @@ -25,14 +25,6 @@ export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( ExternalIncidentServiceSecretConfiguration ); -export const ExecutorSubActionSchema = schema.oneOf([ - schema.literal('getIncident'), - schema.literal('pushToService'), - schema.literal('handshake'), - schema.literal('incidentTypes'), - schema.literal('severity'), -]); - export const ExecutorSubActionPushParamsSchema = schema.object({ incident: schema.object({ name: schema.string(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index 59b0803d189cd..6fec30803d6d7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -24,14 +24,6 @@ export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( ExternalIncidentServiceSecretConfiguration ); -export const ExecutorSubActionSchema = schema.oneOf([ - schema.literal('getFields'), - schema.literal('getIncident'), - schema.literal('pushToService'), - schema.literal('handshake'), - schema.literal('getChoices'), -]); - const CommentsSchema = schema.nullable( schema.arrayOf( schema.object({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts index 1bf37b27ab0d7..0dbfa84789a30 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -25,6 +25,7 @@ export const ConfigMapping = { severityConfig: ConfigMapSchema, }; +export const ExecutorSubActionCommonFieldsParamsSchema = schema.object({}); export const ConfigMappingSchema = schema.object(ConfigMapping); export const SwimlaneServiceConfiguration = { @@ -42,11 +43,6 @@ export const SwimlaneSecretsConfiguration = { export const SwimlaneSecretsConfigurationSchema = schema.object(SwimlaneSecretsConfiguration); -export const ExecutorSubActionSchema = schema.oneOf([ - schema.literal('application'), - schema.literal('createRecord'), -]); - export const ExecutorSubActionCreateRecordParamsSchema = schema.object({ alertName: schema.nullable(schema.string()), alertSource: schema.nullable(schema.string()), diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts index 22f2297497798..65097e9870da1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts @@ -15,6 +15,7 @@ import { ExecutorParamsSchema, ExecutorSubActionCreateRecordParamsSchema, ExecutorSubActionGetApplicationParamsSchema, + ExecutorSubActionCommonFieldsParamsSchema, ConfigMappingSchema, } from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; @@ -30,6 +31,9 @@ export type ExecutorParams = TypeOf; export type ExecutorSubActionCreateRecordParams = TypeOf< typeof ExecutorSubActionCreateRecordParamsSchema >; +export type ExecutorSubActionCommonFieldsParams = TypeOf< + typeof ExecutorSubActionCommonFieldsParamsSchema +>; export interface ExternalServiceCredentials { config: SwimlanePublicConfigurationType; @@ -95,3 +99,8 @@ export interface SwimlaneRecordPayload { values?: SwimlaneDataValues; comments?: SwimlaneDataComments; } + +export interface GetCommonFieldsHandlerArgs { + externalService: ExternalService; + params: ExecutorSubActionCommonFieldsParams; +} diff --git a/x-pack/plugins/cases/README.md b/x-pack/plugins/cases/README.md index 14afe89829a68..7c104279350ae 100644 --- a/x-pack/plugins/cases/README.md +++ b/x-pack/plugins/cases/README.md @@ -211,7 +211,7 @@ This action type has no `secrets` properties. | -------- | ------------------------------------------------------------------------------------------------- | ----------------- | | id | ID of the connector used for pushing case updates to external systems. | string | | name | The connector name. | string | -| type | The type of the connector. Must be one of these: `.servicenow`, `jira`, `.resilient`, and `.none` | string | +| type | The type of the connector. Must be one of these: `.servicenow`, `.servicenow-sir`, `.swimlane`, `jira`, `.resilient`, and `.none` | string | | fields | Object containing the connector’s fields. | [fields](#fields) | #### `fields` diff --git a/x-pack/plugins/cases/common/api/connectors/index.ts b/x-pack/plugins/cases/common/api/connectors/index.ts index 96c5c52399b0e..b8c9500330c70 100644 --- a/x-pack/plugins/cases/common/api/connectors/index.ts +++ b/x-pack/plugins/cases/common/api/connectors/index.ts @@ -12,12 +12,14 @@ import { JiraFieldsRT } from './jira'; import { ResilientFieldsRT } from './resilient'; import { ServiceNowITSMFieldsRT } from './servicenow_itsm'; import { ServiceNowSIRFieldsRT } from './servicenow_sir'; +import { SwimlaneFieldsRT } from './swimlane'; export * from './jira'; export * from './servicenow_itsm'; export * from './servicenow_sir'; export * from './resilient'; export * from './mappings'; +export * from './swimlane'; export type ActionConnector = ActionResult; export type ActionTypeConnector = ActionType; @@ -54,6 +56,11 @@ const ConnectorServiceNowITSMTypeFieldsRt = rt.type({ fields: rt.union([ServiceNowITSMFieldsRT, rt.null]), }); +const ConnectorSwimlaneTypeFieldsRt = rt.type({ + type: rt.literal(ConnectorTypes.swimlane), + fields: rt.union([SwimlaneFieldsRT, rt.null]), +}); + const ConnectorServiceNowSIRTypeFieldsRt = rt.type({ type: rt.literal(ConnectorTypes.serviceNowSIR), fields: rt.union([ServiceNowSIRFieldsRT, rt.null]), @@ -66,10 +73,11 @@ const ConnectorNoneTypeFieldsRt = rt.type({ export const ConnectorTypeFieldsRt = rt.union([ ConnectorJiraTypeFieldsRt, + ConnectorNoneTypeFieldsRt, ConnectorResillientTypeFieldsRt, ConnectorServiceNowITSMTypeFieldsRt, ConnectorServiceNowSIRTypeFieldsRt, - ConnectorNoneTypeFieldsRt, + ConnectorSwimlaneTypeFieldsRt, ]); export const CaseConnectorRt = rt.intersection([ @@ -84,6 +92,7 @@ export type CaseConnector = rt.TypeOf; export type ConnectorTypeFields = rt.TypeOf; export type ConnectorJiraTypeFields = rt.TypeOf; export type ConnectorResillientTypeFields = rt.TypeOf; +export type ConnectorSwimlaneTypeFields = rt.TypeOf; export type ConnectorServiceNowITSMTypeFields = rt.TypeOf< typeof ConnectorServiceNowITSMTypeFieldsRt >; diff --git a/x-pack/plugins/cases/common/api/connectors/mappings.ts b/x-pack/plugins/cases/common/api/connectors/mappings.ts index 3d2013af47688..9933c106befa8 100644 --- a/x-pack/plugins/cases/common/api/connectors/mappings.ts +++ b/x-pack/plugins/cases/common/api/connectors/mappings.ts @@ -26,7 +26,7 @@ export type ThirdPartyField = rt.TypeOf; export const ConnectorMappingsAttributesRT = rt.type({ action_type: ActionTypeRT, source: CaseFieldRT, - target: ThirdPartyFieldRT, + target: rt.union([ThirdPartyFieldRT, rt.undefined]), }); export const ConnectorMappingsRt = rt.type({ @@ -47,9 +47,6 @@ const ConnectorFieldRt = rt.type({ export type ConnectorField = rt.TypeOf; -const GetFieldsResponseRt = rt.type({ - defaultMappings: rt.array(ConnectorMappingsAttributesRT), - fields: rt.array(ConnectorFieldRt), -}); +const GetDefaultMappingsResponseRt = rt.array(ConnectorMappingsAttributesRT); -export type GetFieldsResponse = rt.TypeOf; +export type GetDefaultMappingsResponse = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index e8fe2271060bd..a471d3095183e 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -44,6 +44,7 @@ export const SUPPORTED_CONNECTORS = [ ConnectorTypes.serviceNowSIR, ConnectorTypes.jira, ConnectorTypes.resilient, + ConnectorTypes.swimlane, ]; /** diff --git a/x-pack/plugins/cases/public/components/connectors/config.ts b/x-pack/plugins/cases/public/components/connectors/config.ts index da9989dd53694..f45b173566653 100644 --- a/x-pack/plugins/cases/public/components/connectors/config.ts +++ b/x-pack/plugins/cases/public/components/connectors/config.ts @@ -10,6 +10,7 @@ import { getServiceNowITSMActionType, getServiceNowSIRActionType, getJiraActionType, + getSwimlaneActionType, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../triggers_actions_ui/public/common'; import { ConnectorConfiguration } from './types'; @@ -19,6 +20,7 @@ const resilient = getResilientActionType(); const serviceNowITSM = getServiceNowITSMActionType(); const serviceNowSIR = getServiceNowSIRActionType(); const jira = getJiraActionType(); +const swimlane = getSwimlaneActionType(); export const connectorsConfiguration: Record = { [ConnectorTypes.serviceNowITSM]: { @@ -37,4 +39,8 @@ export const connectorsConfiguration: Record = { name: resilient.actionTypeTitle ?? '', logo: resilient.iconClass, }, + [ConnectorTypes.swimlane]: { + name: swimlane.actionTypeTitle ?? '', + logo: swimlane.iconClass, + }, }; diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index 3bd25b6b61bc5..41216897ff853 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -21,7 +21,7 @@ import { import { create } from './cases/create'; import { update } from './cases/update'; import { addComment } from './comments/add'; -import { getFields } from './configure/get_fields'; +import { getDefaultMappings } from './configure/get_default_mappings'; import { getMappings } from './configure/get_mappings'; import { updateAlertsStatus } from './alerts/update_status'; import { @@ -129,9 +129,9 @@ export class CasesClientHandler implements CasesClient { } } - public async getFields(fields: ConfigureFields) { + public async getDefaultMappings(fields: ConfigureFields) { try { - return getFields(fields); + return getDefaultMappings(fields); } catch (error) { throw createCaseError({ message: `Failed to retrieve fields using client: ${error}`, diff --git a/x-pack/plugins/cases/server/client/configure/get_default_mappings.ts b/x-pack/plugins/cases/server/client/configure/get_default_mappings.ts new file mode 100644 index 0000000000000..43b345da341d2 --- /dev/null +++ b/x-pack/plugins/cases/server/client/configure/get_default_mappings.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; + +import { ConnectorTypes, GetDefaultMappingsResponse } from '../../../common'; +import { ConfigureFields } from '../types'; +import { createDefaultMapping, mapSwimlaneFields, SwimlaneGetFieldsResponse } from './utils'; +import { ActionResult } from '../../../../actions/server'; + +export const getDefaultMappings = async ({ + actionsClient, + connectorType, + connectorId, +}: ConfigureFields): Promise => { + let fields; + if (connectorType === ConnectorTypes.swimlane) { + const results: ActionResult = await actionsClient.get({ + id: connectorId, + }); + if (results.config && results.config.mappings) { + fields = mapSwimlaneFields(results.config.mappings); + } else { + throw Boom.failedDependency('Something is wrong with the Swimlane connector field mappings.'); + } + } + return createDefaultMapping(connectorType, fields); +}; diff --git a/x-pack/plugins/cases/server/client/configure/get_fields.test.ts b/x-pack/plugins/cases/server/client/configure/get_fields.test.ts index c474361293da4..c4914acdb04a8 100644 --- a/x-pack/plugins/cases/server/client/configure/get_fields.test.ts +++ b/x-pack/plugins/cases/server/client/configure/get_fields.test.ts @@ -19,7 +19,7 @@ describe('get_fields', () => { }); describe('happy path', () => { - test('it gets fields', async () => { + test('it gets default mappings', async () => { const savedObjectsClient = createMockSavedObjectsRepository({ caseMappingsSavedObject: mockCaseMappings, }); @@ -30,10 +30,6 @@ describe('get_fields', () => { connectorId: '123', }); expect(res).toEqual({ - fields: [ - { id: 'summary', name: 'Summary', required: true, type: 'text' }, - { id: 'description', name: 'Description', required: false, type: 'text' }, - ], defaultMappings: mappings[ConnectorTypes.jira], }); }); diff --git a/x-pack/plugins/cases/server/client/configure/get_fields.ts b/x-pack/plugins/cases/server/client/configure/get_fields.ts deleted file mode 100644 index 8d899f0df1a76..0000000000000 --- a/x-pack/plugins/cases/server/client/configure/get_fields.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import Boom from '@hapi/boom'; - -import { GetFieldsResponse } from '../../../common'; -import { ConfigureFields } from '../types'; -import { createDefaultMapping, formatFields } from './utils'; - -export const getFields = async ({ - actionsClient, - connectorType, - connectorId, -}: ConfigureFields): Promise => { - const results = await actionsClient.execute({ - actionId: connectorId, - params: { - subAction: 'getFields', - subActionParams: {}, - }, - }); - if (results.status === 'error') { - throw Boom.failedDependency(results.serviceMessage); - } - const fields = formatFields(results.data, connectorType); - - return { fields, defaultMappings: createDefaultMapping(fields, connectorType) }; -}; diff --git a/x-pack/plugins/cases/server/client/configure/get_mappings.ts b/x-pack/plugins/cases/server/client/configure/get_mappings.ts index 3560bf1dcd067..3b9f9f3f50f58 100644 --- a/x-pack/plugins/cases/server/client/configure/get_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/get_mappings.ts @@ -53,7 +53,7 @@ export const getMappings = async ({ (myConnectorMappings.total > 0 && !myConnectorMappings.saved_objects[0].attributes.hasOwnProperty('mappings')) ) { - const res = await casesClient.getFields({ + const res = await casesClient.getDefaultMappings({ actionsClient, connectorId, connectorType, @@ -61,7 +61,7 @@ export const getMappings = async ({ theMapping = await connectorMappingsService.post({ client: savedObjectsClient, attributes: { - mappings: res.defaultMappings, + mappings: res, }, references: [ { diff --git a/x-pack/plugins/cases/server/client/configure/utils.ts b/x-pack/plugins/cases/server/client/configure/utils.ts index 24efb6ca54b3a..9776ee3ce23fc 100644 --- a/x-pack/plugins/cases/server/client/configure/utils.ts +++ b/x-pack/plugins/cases/server/client/configure/utils.ts @@ -10,9 +10,10 @@ import { JiraGetFieldsResponse, ResilientGetFieldsResponse, ServiceNowGetFieldsResponse, + SwimlaneGetFieldsResponse, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../actions/server/types'; - +export { SwimlaneGetFieldsResponse }; const normalizeJiraFields = (jiraFields: JiraGetFieldsResponse): ConnectorField[] => Object.keys(jiraFields).reduce( (acc, data) => @@ -59,7 +60,19 @@ const normalizeServiceNowFields = (snFields: ServiceNowGetFieldsResponse): Conne ], [] ); +export interface SwimlaneMappings { + title: string; + description?: string; + comments?: string; +} +export const mapSwimlaneFields = (slFields: SwimlaneGetFieldsResponse): SwimlaneMappings => ({ + title: slFields.alertNameConfig.id, + description: slFields.commentsConfig?.id, + comments: slFields.commentsConfig?.id, +}); +// unused but lets keep for dynamic field mappings +// https://github.com/elastic/security-team/issues/596 export const formatFields = (theData: unknown, theType: string): ConnectorField[] => { switch (theType) { case ConnectorTypes.jira: @@ -75,7 +88,7 @@ export const formatFields = (theData: unknown, theType: string): ConnectorField[ } }; -const getPreferredFields = (theType: string) => { +const getPreferredFields = (theType: string, swimlaneMappings?: SwimlaneMappings) => { let title: string = ''; let description: string = ''; let comments: string = ''; @@ -95,16 +108,18 @@ const getPreferredFields = (theType: string) => { title = 'short_description'; description = 'description'; comments = 'work_notes'; + } else if (theType === ConnectorTypes.swimlane && swimlaneMappings != null) { + return swimlaneMappings; } return { title, description, comments }; }; export const createDefaultMapping = ( - fields: ConnectorField[], - theType: string + theType: string, + swimlaneMappings?: SwimlaneMappings ): ConnectorMappingsAttributes[] => { - const { description, title, comments } = getPreferredFields(theType); + const { description, title, comments } = getPreferredFields(theType, swimlaneMappings); return [ { source: 'title', diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index 3311b7ac6f921..1e263a2bdb4fb 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -15,18 +15,18 @@ import { CaseStatuses, CommentRequest, ConnectorMappingsAttributes, - GetFieldsResponse, + GetDefaultMappingsResponse, CaseUserActionsResponse, User, } from '../../common'; import { AlertInfo } from '../common'; import { + AlertServiceContract, CaseConfigureServiceSetup, CaseServiceSetup, CaseUserActionServiceSetup, - AlertServiceContract, + ConnectorMappingsServiceSetup, } from '../services'; -import { ConnectorMappingsServiceSetup } from '../services/connector_mappings'; import { CasesClientGetAlertsResponse } from './alerts/types'; export interface CasesClientGet { @@ -100,7 +100,7 @@ export interface CasesClient { create(theCase: CasePostRequest): Promise; get(args: CasesClientGet): Promise; getAlerts(args: CasesClientGetAlerts): Promise; - getFields(args: ConfigureFields): Promise; + getDefaultMappings(args: ConfigureFields): Promise; getMappings(args: MappingsClient): Promise; getUserActions(args: CasesClientGetUserActions): Promise; push(args: CasesClientPush): Promise; diff --git a/x-pack/plugins/cases/server/connectors/index.ts b/x-pack/plugins/cases/server/connectors/index.ts index d8f5479530827..82b29207866ae 100644 --- a/x-pack/plugins/cases/server/connectors/index.ts +++ b/x-pack/plugins/cases/server/connectors/index.ts @@ -18,6 +18,7 @@ import { serviceNowSIRExternalServiceFormatter } from './servicenow/sir_formatte import { jiraExternalServiceFormatter } from './jira/external_service_formatter'; import { resilientExternalServiceFormatter } from './resilient/external_service_formatter'; import { CommentRequest, CommentType, ConnectorTypes } from '../../common'; +import { swimlaneExternalServiceFormatter } from './swimlane/external_service_formatter'; export * from './types'; export { transformConnectorComment } from './case'; @@ -54,6 +55,7 @@ export const externalServiceFormatters: ExternalServiceFormatterMapper = { [ConnectorTypes.serviceNowSIR]: serviceNowSIRExternalServiceFormatter, [ConnectorTypes.jira]: jiraExternalServiceFormatter, [ConnectorTypes.resilient]: resilientExternalServiceFormatter, + [ConnectorTypes.swimlane]: swimlaneExternalServiceFormatter, }; export const isCommentGeneratedAlert = ( diff --git a/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts b/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts index e69de29bb2d1d..5225e7685c0c8 100644 --- a/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts +++ b/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts @@ -0,0 +1,25 @@ +/* + * 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 { ExternalServiceFormatter } from '../types'; +import { ConnectorSwimlaneTypeFields, SwimlaneFieldsType } from '../../../common'; + +const format: ExternalServiceFormatter['format'] = (theCase) => { + const { + alertName = '', + alertSource = null, + caseId = null, + caseName = null, + comments = null, + severity = null, + } = (theCase.connector.fields as ConnectorSwimlaneTypeFields['fields']) ?? {}; + return { alertName, alertSource, caseId, caseName, comments, severity }; +}; + +export const swimlaneExternalServiceFormatter: ExternalServiceFormatter = { + format, +}; diff --git a/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts index 8d642e47eabab..f26f394dc48e6 100644 --- a/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts +++ b/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts @@ -125,6 +125,14 @@ export const getActionTypes = (): ActionTypeConnector[] => [ enabledInConfig: true, enabledInLicense: true, }, + { + id: ConnectorTypes.swimlane, + name: 'Swimlane', + minimumLicenseRequired: 'gold', + enabled: false, + enabledInConfig: true, + enabledInLicense: true, + }, ]; export const getActionExecuteResults = (actionId = '123') => ({ diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.ts index 7aec7e4f086b4..ddd0eeca0b02f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.ts @@ -12,13 +12,17 @@ import { ActionType } from '../../../../../../actions/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { FindActionResult } from '../../../../../../actions/server/types'; -import { CASE_CONFIGURE_CONNECTORS_URL, SUPPORTED_CONNECTORS } from '../../../../../common'; +import { + CASE_CONFIGURE_CONNECTORS_URL, + ConnectorTypes, + SUPPORTED_CONNECTORS, +} from '../../../../../common'; const isConnectorSupported = ( action: FindActionResult, actionTypes: Record ): boolean => - SUPPORTED_CONNECTORS.includes(action.actionTypeId) && + SUPPORTED_CONNECTORS.includes(action.actionTypeId as ConnectorTypes) && actionTypes[action.actionTypeId]?.enabledInLicense; /* diff --git a/x-pack/plugins/triggers_actions_ui/public/common/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/index.ts index 01470bdddf4d7..375634e9c1aec 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/index.ts @@ -17,3 +17,4 @@ export { } from '../application/components/builtin_action_types/servicenow'; export { getJiraActionType } from '../application/components/builtin_action_types/jira'; export { getResilientActionType } from '../application/components/builtin_action_types/resilient'; +export { getSwimlaneActionType } from '../application/components/builtin_action_types/swimlane'; From fa1f7493d19078a1c28bead31d65015ba04f002f Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 3 May 2021 15:40:57 -0600 Subject: [PATCH 06/96] fix types --- x-pack/plugins/actions/server/index.ts | 1 - x-pack/plugins/actions/server/types.ts | 2 +- x-pack/plugins/cases/common/constants.ts | 10 +++++----- x-pack/plugins/cases/server/client/cases/utils.ts | 1 + .../client/configure/get_default_mappings.ts | 10 ++++++---- ...ields.test.ts => get_default_mapppings.test.ts} | 10 ++++------ .../cases/server/client/configure/utils.test.ts | 4 ++-- .../plugins/cases/server/client/configure/utils.ts | 8 +++++--- x-pack/plugins/cases/server/client/mocks.ts | 2 +- .../cases/server/connectors/case/index.test.ts | 14 +++++++------- .../routes/api/cases/configure/get_connectors.ts | 8 ++------ 11 files changed, 34 insertions(+), 36 deletions(-) rename x-pack/plugins/cases/server/client/configure/{get_fields.test.ts => get_default_mapppings.test.ts} (90%) diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index b16bebda4de29..5633bc321bfe6 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -47,7 +47,6 @@ export type { TeamsActionTypeId, TeamsActionParams, } from './builtin_action_types'; - export type { PluginSetupContract, PluginStartContract } from './plugin'; export { asSavedObjectExecutionSource, asHttpRequestExecutionSource } from './lib'; diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index ea22e90dfed40..f5da3e73c6d62 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -22,7 +22,7 @@ export { ActionTypeExecutorResult } from '../common'; export { GetFieldsByIssueTypeResponse as JiraGetFieldsResponse } from './builtin_action_types/jira/types'; export { GetCommonFieldsResponse as ServiceNowGetFieldsResponse } from './builtin_action_types/servicenow/types'; export { GetCommonFieldsResponse as ResilientGetFieldsResponse } from './builtin_action_types/resilient/types'; - +export { SwimlanePublicConfigurationType } from './builtin_action_types/swimlane/types'; export type WithoutQueryAndParams = Pick>; export type GetServicesFunction = (request: KibanaRequest) => Services; export type ActionTypeRegistryContract = PublicMethodsOf; diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index a471d3095183e..4d1d56fab8c40 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -40,11 +40,11 @@ export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`; export const ACTION_URL = '/api/actions'; export const ACTION_TYPES_URL = '/api/actions/list_action_types'; export const SUPPORTED_CONNECTORS = [ - ConnectorTypes.serviceNowITSM, - ConnectorTypes.serviceNowSIR, - ConnectorTypes.jira, - ConnectorTypes.resilient, - ConnectorTypes.swimlane, + `${ConnectorTypes.serviceNowITSM}`, + `${ConnectorTypes.serviceNowSIR}`, + `${ConnectorTypes.jira}`, + `${ConnectorTypes.resilient}`, + `${ConnectorTypes.swimlane}`, ]; /** diff --git a/x-pack/plugins/cases/server/client/cases/utils.ts b/x-pack/plugins/cases/server/client/cases/utils.ts index 9bfad7ddcec3c..20f7620e6b58d 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.ts @@ -259,6 +259,7 @@ export const prepareFieldsForTransformation = ({ mappings.reduce( (acc: PipedField[], mapping) => mapping != null && + mapping.target != null && // TODO put warning if no target mapping.target !== 'not_mapped' && mapping.action_type !== 'nothing' && mapping.source !== 'comments' diff --git a/x-pack/plugins/cases/server/client/configure/get_default_mappings.ts b/x-pack/plugins/cases/server/client/configure/get_default_mappings.ts index 43b345da341d2..c165f134cfcf4 100644 --- a/x-pack/plugins/cases/server/client/configure/get_default_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/get_default_mappings.ts @@ -9,8 +9,7 @@ import Boom from '@hapi/boom'; import { ConnectorTypes, GetDefaultMappingsResponse } from '../../../common'; import { ConfigureFields } from '../types'; -import { createDefaultMapping, mapSwimlaneFields, SwimlaneGetFieldsResponse } from './utils'; -import { ActionResult } from '../../../../actions/server'; +import { createDefaultMapping, mapSwimlaneFields, SwimlanePublicConfigurationType } from './utils'; export const getDefaultMappings = async ({ actionsClient, @@ -19,11 +18,14 @@ export const getDefaultMappings = async ({ }: ConfigureFields): Promise => { let fields; if (connectorType === ConnectorTypes.swimlane) { - const results: ActionResult = await actionsClient.get({ + const results = await actionsClient.get({ id: connectorId, }); + if (results.config && results.config.mappings) { - fields = mapSwimlaneFields(results.config.mappings); + fields = mapSwimlaneFields( + (results.config.mappings as unknown) as SwimlanePublicConfigurationType['mappings'] + ); } else { throw Boom.failedDependency('Something is wrong with the Swimlane connector field mappings.'); } diff --git a/x-pack/plugins/cases/server/client/configure/get_fields.test.ts b/x-pack/plugins/cases/server/client/configure/get_default_mapppings.test.ts similarity index 90% rename from x-pack/plugins/cases/server/client/configure/get_fields.test.ts rename to x-pack/plugins/cases/server/client/configure/get_default_mapppings.test.ts index c4914acdb04a8..bffd48b0f06f1 100644 --- a/x-pack/plugins/cases/server/client/configure/get_fields.test.ts +++ b/x-pack/plugins/cases/server/client/configure/get_default_mapppings.test.ts @@ -11,7 +11,7 @@ import { createMockSavedObjectsRepository, mockCaseMappings } from '../../routes import { createCasesClientWithMockSavedObjectsClient } from '../mocks'; import { actionsClientMock } from '../../../../actions/server/actions_client.mock'; import { actionsErrResponse, mappings, mockGetFieldsResponse } from './mock'; -describe('get_fields', () => { +describe('get_default_mappings', () => { const execute = jest.fn().mockReturnValue(mockGetFieldsResponse); const actionsMock = { ...actionsClientMock.create(), execute }; beforeEach(async () => { @@ -24,14 +24,12 @@ describe('get_fields', () => { caseMappingsSavedObject: mockCaseMappings, }); const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - const res = await casesClient.client.getFields({ + const res = await casesClient.client.getDefaultMappings({ actionsClient: actionsMock, connectorType: ConnectorTypes.jira, connectorId: '123', }); - expect(res).toEqual({ - defaultMappings: mappings[ConnectorTypes.jira], - }); + expect(res).toEqual(mappings[ConnectorTypes.jira]); }); }); @@ -42,7 +40,7 @@ describe('get_fields', () => { }); const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); await casesClient.client - .getFields({ + .getDefaultMappings({ actionsClient: { ...actionsMock, execute: jest.fn().mockReturnValue(actionsErrResponse) }, connectorType: ConnectorTypes.jira, connectorId: '123', diff --git a/x-pack/plugins/cases/server/client/configure/utils.test.ts b/x-pack/plugins/cases/server/client/configure/utils.test.ts index 41d62f5a9b91f..60417de039ba7 100644 --- a/x-pack/plugins/cases/server/client/configure/utils.test.ts +++ b/x-pack/plugins/cases/server/client/configure/utils.test.ts @@ -23,9 +23,9 @@ describe('client/configure/utils', () => { }); }); describe('createDefaultMapping', () => { - formatFieldsTestData.forEach(({ expected, fields, type }) => { + formatFieldsTestData.forEach(({ type }) => { it(`normalizes ${type} fields to common type ConnectorField`, () => { - const result = createDefaultMapping(expected, type); + const result = createDefaultMapping(type); expect(result).toEqual(mappings[type]); }); }); diff --git a/x-pack/plugins/cases/server/client/configure/utils.ts b/x-pack/plugins/cases/server/client/configure/utils.ts index 9776ee3ce23fc..f31fc8b2f85c1 100644 --- a/x-pack/plugins/cases/server/client/configure/utils.ts +++ b/x-pack/plugins/cases/server/client/configure/utils.ts @@ -10,10 +10,10 @@ import { JiraGetFieldsResponse, ResilientGetFieldsResponse, ServiceNowGetFieldsResponse, - SwimlaneGetFieldsResponse, + SwimlanePublicConfigurationType, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../actions/server/types'; -export { SwimlaneGetFieldsResponse }; +export { SwimlanePublicConfigurationType }; const normalizeJiraFields = (jiraFields: JiraGetFieldsResponse): ConnectorField[] => Object.keys(jiraFields).reduce( (acc, data) => @@ -65,7 +65,9 @@ export interface SwimlaneMappings { description?: string; comments?: string; } -export const mapSwimlaneFields = (slFields: SwimlaneGetFieldsResponse): SwimlaneMappings => ({ +export const mapSwimlaneFields = ( + slFields: SwimlanePublicConfigurationType['mappings'] +): SwimlaneMappings => ({ title: slFields.alertNameConfig.id, description: slFields.commentsConfig?.id, comments: slFields.commentsConfig?.id, diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index 4c0f89cf77a67..58042afc7c814 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -26,7 +26,7 @@ export const createExternalCasesClientMock = (): CasesClientPluginContractMock = get: jest.fn(), push: jest.fn(), getAlerts: jest.fn(), - getFields: jest.fn(), + getDefaultMappings: jest.fn(), getMappings: jest.fn(), getUserActions: jest.fn(), update: jest.fn(), diff --git a/x-pack/plugins/cases/server/connectors/case/index.test.ts b/x-pack/plugins/cases/server/connectors/case/index.test.ts index 2415569392125..2ee2d23f7fe51 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts @@ -70,7 +70,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { issueType: '10006', priority: 'High', @@ -109,7 +109,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { issueType: '10006', priority: 'High', @@ -303,7 +303,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { priority: 'High', parent: null, @@ -448,7 +448,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { issueType: '10006', priority: 'High', @@ -650,7 +650,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { priority: 'High', parent: null, @@ -980,7 +980,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { issueType: '10006', priority: 'High', @@ -1009,7 +1009,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { issueType: '10006', priority: 'High', diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.ts index ddd0eeca0b02f..7aec7e4f086b4 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.ts @@ -12,17 +12,13 @@ import { ActionType } from '../../../../../../actions/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { FindActionResult } from '../../../../../../actions/server/types'; -import { - CASE_CONFIGURE_CONNECTORS_URL, - ConnectorTypes, - SUPPORTED_CONNECTORS, -} from '../../../../../common'; +import { CASE_CONFIGURE_CONNECTORS_URL, SUPPORTED_CONNECTORS } from '../../../../../common'; const isConnectorSupported = ( action: FindActionResult, actionTypes: Record ): boolean => - SUPPORTED_CONNECTORS.includes(action.actionTypeId as ConnectorTypes) && + SUPPORTED_CONNECTORS.includes(action.actionTypeId) && actionTypes[action.actionTypeId]?.enabledInLicense; /* From 796712c38abd2bf9c1d5eda9860fd1d8a9847d2f Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 3 May 2021 19:15:02 -0600 Subject: [PATCH 07/96] working on fields --- .../builtin_action_types/swimlane/schema.ts | 1 - .../builtin_action_types/swimlane/types.ts | 9 -- .../cases/common/api/connectors/index.ts | 4 +- .../cases/common/api/connectors/swimlane.ts | 15 +- .../public/components/connectors/index.ts | 3 + .../connectors/swimlane/case_fields.test.tsx | 134 ++++++++++++++++++ .../connectors/swimlane/case_fields.tsx | 100 +++++++++++++ .../components/connectors/swimlane/index.ts | 28 ++++ .../connectors/swimlane/translations.ts | 27 ++++ .../components/connectors/swimlane/types.ts | 9 ++ 10 files changed, 313 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/swimlane/index.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/swimlane/types.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts index 0dbfa84789a30..e4fb6e09b0e11 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -25,7 +25,6 @@ export const ConfigMapping = { severityConfig: ConfigMapSchema, }; -export const ExecutorSubActionCommonFieldsParamsSchema = schema.object({}); export const ConfigMappingSchema = schema.object(ConfigMapping); export const SwimlaneServiceConfiguration = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts index 65097e9870da1..22f2297497798 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts @@ -15,7 +15,6 @@ import { ExecutorParamsSchema, ExecutorSubActionCreateRecordParamsSchema, ExecutorSubActionGetApplicationParamsSchema, - ExecutorSubActionCommonFieldsParamsSchema, ConfigMappingSchema, } from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; @@ -31,9 +30,6 @@ export type ExecutorParams = TypeOf; export type ExecutorSubActionCreateRecordParams = TypeOf< typeof ExecutorSubActionCreateRecordParamsSchema >; -export type ExecutorSubActionCommonFieldsParams = TypeOf< - typeof ExecutorSubActionCommonFieldsParamsSchema ->; export interface ExternalServiceCredentials { config: SwimlanePublicConfigurationType; @@ -99,8 +95,3 @@ export interface SwimlaneRecordPayload { values?: SwimlaneDataValues; comments?: SwimlaneDataComments; } - -export interface GetCommonFieldsHandlerArgs { - externalService: ExternalService; - params: ExecutorSubActionCommonFieldsParams; -} diff --git a/x-pack/plugins/cases/common/api/connectors/index.ts b/x-pack/plugins/cases/common/api/connectors/index.ts index b8c9500330c70..40763d94c3e81 100644 --- a/x-pack/plugins/cases/common/api/connectors/index.ts +++ b/x-pack/plugins/cases/common/api/connectors/index.ts @@ -12,7 +12,7 @@ import { JiraFieldsRT } from './jira'; import { ResilientFieldsRT } from './resilient'; import { ServiceNowITSMFieldsRT } from './servicenow_itsm'; import { ServiceNowSIRFieldsRT } from './servicenow_sir'; -import { SwimlaneFieldsRT } from './swimlane'; +import { SwimlaneUnmappedFieldsRT } from './swimlane'; export * from './jira'; export * from './servicenow_itsm'; @@ -58,7 +58,7 @@ const ConnectorServiceNowITSMTypeFieldsRt = rt.type({ const ConnectorSwimlaneTypeFieldsRt = rt.type({ type: rt.literal(ConnectorTypes.swimlane), - fields: rt.union([SwimlaneFieldsRT, rt.null]), + fields: rt.union([SwimlaneUnmappedFieldsRT, rt.null]), }); const ConnectorServiceNowSIRTypeFieldsRt = rt.type({ diff --git a/x-pack/plugins/cases/common/api/connectors/swimlane.ts b/x-pack/plugins/cases/common/api/connectors/swimlane.ts index 92c07ff17bbc8..bdd84b221b3cf 100644 --- a/x-pack/plugins/cases/common/api/connectors/swimlane.ts +++ b/x-pack/plugins/cases/common/api/connectors/swimlane.ts @@ -6,15 +6,20 @@ */ import * as rt from 'io-ts'; - -// New fields should also be added at: x-pack/plugins/cases/server/connectors/case/schema.ts -export const SwimlaneFieldsRT = rt.type({ - alertName: rt.string, +export const SwimlaneUnmappedFieldsRT = rt.type({ alertSource: rt.union([rt.string, rt.null]), caseId: rt.union([rt.string, rt.null]), caseName: rt.union([rt.string, rt.null]), - comments: rt.union([rt.string, rt.null]), severity: rt.union([rt.string, rt.null]), }); +// New fields should also be added at: x-pack/plugins/cases/server/connectors/case/schema.ts +export const SwimlaneFieldsRT = rt.intersection([ + SwimlaneUnmappedFieldsRT, + rt.type({ + alertName: rt.string, + comments: rt.union([rt.string, rt.null]), + }), +]); export type SwimlaneFieldsType = rt.TypeOf; +export type SwimlaneUnmappedFieldsType = rt.TypeOf; diff --git a/x-pack/plugins/cases/public/components/connectors/index.ts b/x-pack/plugins/cases/public/components/connectors/index.ts index 71ba161eb63c9..78c2e4ad0d208 100644 --- a/x-pack/plugins/cases/public/components/connectors/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/index.ts @@ -8,6 +8,7 @@ import { CaseConnectorsRegistry } from './types'; import { createCaseConnectorsRegistry } from './connectors_registry'; import { getCaseConnector as getJiraCaseConnector } from './jira'; +import { getCaseConnector as getSwimlaneCaseConnector } from './swimlane'; import { getCaseConnector as getResilientCaseConnector } from './resilient'; import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow'; import { @@ -15,6 +16,7 @@ import { ServiceNowITSMFieldsType, ServiceNowSIRFieldsType, ResilientFieldsType, + SwimlaneUnmappedFieldsType, } from '../../../common'; export { getActionType as getCaseConnectorUi } from './case'; @@ -41,6 +43,7 @@ class CaseConnectors { getServiceNowITSMCaseConnector() ); this.caseConnectorsRegistry.register(getServiceNowSIRCaseConnector()); + this.caseConnectorsRegistry.register(getSwimlaneCaseConnector()); } registry(): CaseConnectorsRegistry { diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx new file mode 100644 index 0000000000000..dda6ba5de95cc --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx @@ -0,0 +1,134 @@ +/* + * 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 React from 'react'; +import { mount } from 'enzyme'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { waitFor } from '@testing-library/react'; + +import { connector } from '../mock'; +import { useGetIncidentTypes } from './use_get_incident_types'; +import { useGetSeverity } from './use_get_severity'; +import Fields from './case_fields'; + +jest.mock('../../../common/lib/kibana'); +jest.mock('./use_get_incident_types'); +jest.mock('./use_get_severity'); + +const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; +const useGetSeverityMock = useGetSeverity as jest.Mock; + +describe('ResilientParamsFields renders', () => { + const useGetIncidentTypesResponse = { + isLoading: false, + incidentTypes: [ + { + id: 19, + name: 'Malware', + }, + { + id: 21, + name: 'Denial of Service', + }, + ], + }; + + const useGetSeverityResponse = { + isLoading: false, + severity: [ + { + id: 4, + name: 'Low', + }, + { + id: 5, + name: 'Medium', + }, + { + id: 6, + name: 'High', + }, + ], + }; + + const fields = { + severityCode: '6', + incidentTypes: ['19'], + }; + + const onChange = jest.fn(); + + beforeEach(() => { + useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse); + useGetSeverityMock.mockReturnValue(useGetSeverityResponse); + jest.clearAllMocks(); + }); + + test('all params fields are rendered', () => { + const wrapper = mount(); + expect(wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('options')).toEqual( + [ + { label: 'Malware', value: '19' }, + { label: 'Denial of Service', value: '21' }, + ] + ); + + expect( + wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('selectedOptions') + ).toEqual([{ label: 'Malware', value: '19' }]); + + expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('value')).toStrictEqual( + '6' + ); + }); + + test('it disabled the fields when loading incident types', () => { + useGetIncidentTypesMock.mockReturnValue({ ...useGetIncidentTypesResponse, isLoading: true }); + + const wrapper = mount(); + + expect( + wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('isDisabled') + ).toBeTruthy(); + }); + + test('it disabled the fields when loading severity', () => { + useGetSeverityMock.mockReturnValue({ + ...useGetSeverityResponse, + isLoading: true, + }); + + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('disabled')).toBeTruthy(); + }); + + test('it sets issue type correctly', async () => { + const wrapper = mount(); + + await waitFor(() => { + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ value: '19', label: 'Denial of Service' }]); + }); + + expect(onChange).toHaveBeenCalledWith({ incidentTypes: ['19'], severityCode: '6' }); + }); + + test('it sets severity correctly', async () => { + const wrapper = mount(); + + wrapper + .find('select[data-test-subj="severitySelect"]') + .first() + .simulate('change', { + target: { value: '4' }, + }); + + expect(onChange).toHaveBeenCalledWith({ incidentTypes: ['19'], severityCode: '4' }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx new file mode 100644 index 0000000000000..2f51d9dddcb93 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx @@ -0,0 +1,100 @@ +/* + * 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 React, { useMemo, useCallback, useState } from 'react'; +import { EuiFieldText, EuiFormRow, EuiSpacer } from '@elastic/eui'; + +import { ConnectorFieldsProps } from '../types'; +import { ConnectorTypes, SwimlaneUnmappedFieldsType } from '../../../../common'; +import { ConnectorCard } from '../card'; +import { fieldLabels } from './index'; +const SwimlaneFieldsComponent: React.FunctionComponent< + ConnectorFieldsProps +> = ({ isEdit = true, fields, connector, onChange }) => { + const [state, setState] = useState( + fields ?? { alertSource: null, caseId: null, caseName: null, severity: null } + ); + const { alertSource, caseId, caseName, severity } = state; + + const onFieldChange = useCallback((key, { target: { value } }) => { + setState((prevState) => ({ + ...prevState, + [key]: value, + })); + }, []); + + const listItems = useMemo( + () => [ + ...(alertSource != null && alertSource.length > 0 + ? [ + { + title: fieldLabels.alertSource, + description: alertSource ?? '', + }, + ] + : []), + ...(caseName != null && caseName.length > 0 + ? [ + { + title: fieldLabels.caseName, + description: caseName ?? '', + }, + ] + : []), + ...(caseId != null && caseId.length > 0 + ? [ + { + title: fieldLabels.caseId, + description: caseId ?? '', + }, + ] + : []), + ...(severity != null && severity.length > 0 + ? [ + { + title: fieldLabels.severity, + description: severity ?? '', + }, + ] + : []), + ], + [alertSource, caseId, caseName, severity] + ); + const Fields = useCallback( + () => ( + + {Object.keys(state).map((f) => ( + <> + + onFieldChange(f, e)} + aria-label="Use aria labels when no actual label is in use" + /> + + + + ))} + + ), + [onFieldChange, state] + ); + + return isEdit ? ( + + ) : ( + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SwimlaneFieldsComponent as default }; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts new file mode 100644 index 0000000000000..7916d509791f9 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts @@ -0,0 +1,28 @@ +/* + * 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 { lazy } from 'react'; + +import { CaseConnector } from '../types'; +import { ConnectorTypes, SwimlaneUnmappedFieldsType } from '../../../../common'; +import * as i18n from './translations'; + +export * from './types'; + +export const getCaseConnector = (): CaseConnector => { + return { + id: ConnectorTypes.swimlane, + fieldsComponent: lazy(() => import('./case_fields')), + }; +}; + +export const fieldLabels = { + alertSource: i18n.ALERT_SOURCE_LABEL, + caseId: i18n.CASE_ID_LABEL, + caseName: i18n.CASE_NAME_LABEL, + severity: i18n.SEVERITY_LABEL, +}; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts new file mode 100644 index 0000000000000..046d9f06112ee --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts @@ -0,0 +1,27 @@ +/* + * 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 ALERT_SOURCE_LABEL = i18n.translate( + 'xpack.cases.connectors.swimlane.alertSourceLabel', + { + defaultMessage: 'Alert Source', + } +); + +export const CASE_ID_LABEL = i18n.translate('xpack.cases.connectors.swimlane.caseIdLabel', { + defaultMessage: 'Incident Types', +}); + +export const CASE_NAME_LABEL = i18n.translate('xpack.cases.connectors.swimlane.caseNameLabel', { + defaultMessage: 'Incident Types', +}); + +export const SEVERITY_LABEL = i18n.translate('xpack.cases.connectors.swimlane.severityLabel', { + defaultMessage: 'Severity', +}); diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/types.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/types.ts new file mode 100644 index 0000000000000..06506d2c0d2f9 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/types.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export type ResilientIncidentTypes = Array<{ id: number; name: string }>; +export type ResilientSeverity = ResilientIncidentTypes; From 377fd0af34630fc1feea19f24b6b87d23adbced2 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 5 May 2021 15:28:52 -0600 Subject: [PATCH 08/96] can push case to swimlane --- .../builtin_action_types/swimlane/api.ts | 51 +++++++- .../builtin_action_types/swimlane/helpers.ts | 2 +- .../builtin_action_types/swimlane/index.ts | 19 ++- .../builtin_action_types/swimlane/schema.ts | 25 +++- .../builtin_action_types/swimlane/service.ts | 38 +++++- .../builtin_action_types/swimlane/types.ts | 56 ++++++--- .../connectors/swimlane/case_fields.tsx | 118 ++++++++++-------- .../connectors/swimlane/translations.ts | 4 +- .../plugins/cases/server/client/cases/get.ts | 1 - .../plugins/cases/server/client/cases/push.ts | 2 +- .../client/configure/get_default_mappings.ts | 24 +--- .../cases/server/client/configure/utils.ts | 13 +- .../swimlane/external_service_formatter.ts | 18 +-- 13 files changed, 248 insertions(+), 123 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts index 8efc1927f5351..bf7ae225d9161 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts @@ -5,15 +5,60 @@ * 2.0. */ -import { CreateRecordApiHandlerArgs, CreateRecordResponse, ExternalServiceApi } from './types'; +import { + CreateRecordApiHandlerArgs, + ExternalServiceIncidentResponse, + ExternalServiceApi, + Incident, + PushToServiceApiHandlerArgs, +} from './types'; const createRecordHandler = async ({ externalService, params, -}: CreateRecordApiHandlerArgs): Promise => { - return await externalService.createRecord(params); +}: CreateRecordApiHandlerArgs): Promise => { + return await externalService.createRecord({ incident: { ...params, externalId: null } }); +}; + +const pushToServiceHandler = async ({ + externalService, + params, +}: PushToServiceApiHandlerArgs): Promise => { + const { comments } = params; + let res: ExternalServiceIncidentResponse; + const incident: Incident = params.incident; + + if (incident.externalId != null) { + res = await externalService.updateRecord({ + incidentId: incident.externalId, + incident, + }); + } else { + res = await externalService.createRecord({ incident }); + } + + console.log('TO DO comments', comments); + // if (comments && Array.isArray(comments) && comments.length > 0) { + // res.comments = []; + // for (const currentComment of comments) { + // const comment = await externalService.createComment({ + // incidentId: res.id, + // comment: currentComment, + // }); + // res.comments = [ + // ...(res.comments ?? []), + // { + // commentId: comment.commentId, + // pushedDate: comment.pushedDate, + // }, + // ]; + // } + // } + + return res; }; export const api: ExternalServiceApi = { createRecord: createRecordHandler, + pushToService: pushToServiceHandler, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts index fdb454cc17e80..93c0cedf87fb0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts @@ -16,7 +16,7 @@ import { export const getBodyForEventAction = ( applicationId: string, mappingConfig: MappingConfigType, - params: CreateRecordParams + params: CreateRecordParams['incident'] ): SwimlaneRecordPayload => { const data: SwimlaneRecordPayload = { applicationId, diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts index adf04d86de1be..1c742bd5a4718 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts @@ -17,6 +17,7 @@ import { SwimlaneSecretConfigurationType, ExecutorParams, ExecutorSubActionCreateRecordParams, + ExecutorSubActionPushParams, } from './types'; import { validate } from './validators'; import { @@ -32,7 +33,7 @@ interface GetActionTypeParams { configurationUtilities: ActionsConfigurationUtilities; } -const supportedSubActions: string[] = ['application', 'createRecord']; +const supportedSubActions: string[] = ['application', 'createRecord', 'pushToService']; // action type definition export function getActionType( @@ -77,7 +78,6 @@ async function executor( ): Promise> { const { actionId, config, params, secrets } = execOptions; const { subAction, subActionParams } = params as ExecutorParams; - let data: SwimlaneExecutorResultData | null = null; const externalService = createExternalService( @@ -112,6 +112,21 @@ async function executor( logger.debug(`Swimlane new record id ${data.id}`); } + if (subAction === 'pushToService') { + const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; + + data = await api.pushToService({ + externalService, + params: pushToServiceParams, + logger, + }); + data = { + ...data, + url: `${config.apiUrl}/record/${config.appId}/${data.id}`, + }; + + logger.debug(`response push to service for incident id: ${data.id}`); + } return { status: 'ok', data: data ?? {}, actionId }; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts index e4fb6e09b0e11..159b4339b74a6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -41,14 +41,29 @@ export const SwimlaneSecretsConfiguration = { }; export const SwimlaneSecretsConfigurationSchema = schema.object(SwimlaneSecretsConfiguration); - -export const ExecutorSubActionCreateRecordParamsSchema = schema.object({ - alertName: schema.nullable(schema.string()), +const SwimlaneFields = { + alertName: schema.string(), alertSource: schema.nullable(schema.string()), caseId: schema.nullable(schema.string()), caseName: schema.nullable(schema.string()), comments: schema.nullable(schema.string()), severity: schema.nullable(schema.string()), +}; +export const ExecutorSubActionCreateRecordParamsSchema = schema.object(SwimlaneFields); + +export const ExecutorSubActionPushParamsSchema = schema.object({ + incident: schema.object({ + ...SwimlaneFields, + externalId: schema.nullable(schema.string()), + }), + comments: schema.nullable( + schema.arrayOf( + schema.object({ + comment: schema.string(), + commentId: schema.string(), + }) + ) + ), }); export const ExecutorSubActionGetApplicationParamsSchema = schema.object({ id: schema.string() }); @@ -58,4 +73,8 @@ export const ExecutorParamsSchema = schema.oneOf([ subAction: schema.literal('createRecord'), subActionParams: ExecutorSubActionCreateRecordParamsSchema, }), + schema.object({ + subAction: schema.literal('pushToService'), + subActionParams: ExecutorSubActionPushParamsSchema, + }), ]); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index 7eb4a1f485426..c941b5a0d6414 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -16,7 +16,7 @@ import { ExternalService, ExternalServiceCredentials, CreateRecordParams, - CreateRecordResponse, + ExternalServiceIncidentResponse, SwimlaneSecretConfigurationType, MappingConfigType, } from './types'; @@ -48,11 +48,12 @@ export const createExternalService = ( const recordUrl = `${apiUrl}/app/{appId}/record`; const getPostRecordUrl = (id: string) => recordUrl.replace('{appId}', id); - const createRecord = async (params: CreateRecordParams): Promise => { + const createRecord = async ( + params: CreateRecordParams + ): Promise => { try { const mappingConfig = mappings as MappingConfigType; - const data = getBodyForEventAction(appId, mappingConfig, params); - console.log('data', JSON.stringify(data)); + const data = getBodyForEventAction(appId, mappingConfig, params.incident); const res = await request({ axios: axiosInstance, url: getPostRecordUrl(appId), @@ -62,7 +63,33 @@ export const createExternalService = ( method: 'post', data, }); - return { id: res.data.id }; + return { id: res.data.id, title: res.data.name }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to create record in application with id ${appId}. Error: ${error.message}` + ) + ); + } + }; + + const updateRecord = async ( + params: CreateRecordParams + ): Promise => { + try { + const mappingConfig = mappings as MappingConfigType; + const data = getBodyForEventAction(appId, mappingConfig, params.incident); + const res = await request({ + axios: axiosInstance, + url: getPostRecordUrl(appId), + logger, + configurationUtilities, + headers, + method: 'post', + data, + }); + return { id: res.data.id, title: res.data.name }; } catch (error) { throw new Error( getErrorMessage( @@ -75,5 +102,6 @@ export const createExternalService = ( return { createRecord, + updateRecord, }; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts index 22f2297497798..f1bb466468e49 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts @@ -10,12 +10,13 @@ import { TypeOf } from '@kbn/config-schema'; import { Logger } from '@kbn/logging'; import { - SwimlaneSecretsConfigurationSchema, - SwimlaneServiceConfigurationSchema, + ConfigMappingSchema, ExecutorParamsSchema, ExecutorSubActionCreateRecordParamsSchema, ExecutorSubActionGetApplicationParamsSchema, - ConfigMappingSchema, + ExecutorSubActionPushParamsSchema, + SwimlaneSecretsConfigurationSchema, + SwimlaneServiceConfigurationSchema, } from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; @@ -26,6 +27,7 @@ export type MappingConfigType = TypeOf & Record; export type ExecutorParams = TypeOf; +export type ExecutorSubActionPushParams = TypeOf; export type ExecutorSubActionCreateRecordParams = TypeOf< typeof ExecutorSubActionCreateRecordParamsSchema @@ -41,11 +43,22 @@ export interface ExternalServiceValidation { secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void; } -export type CreateRecordParams = TypeOf & - Record; +export interface CreateRecordParams { + incident: Incident; +} +export interface UpdateRecordParams extends CreateRecordParams { + incidentId: string; +} + +export type PushToServiceApiParams = ExecutorSubActionPushParams; +export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: PushToServiceApiParams; + logger: Logger; +} -export interface CreateRecordResponse { +export interface ExternalServiceIncidentResponse { id: string; + title: string; } export interface FieldConfig { @@ -55,10 +68,23 @@ export interface FieldConfig { fieldType: string; } +export interface SwimlaneRecordPayload { + applicationId: string; + values?: SwimlaneDataValues; + comments?: SwimlaneDataComments; +} + +export interface PushToServiceResponse extends ExternalServiceIncidentResponse { + url: string; +} + +// export type ExternalServiceParams = Record; export interface ExternalService { - createRecord: (params: CreateRecordParams) => Promise; + createRecord: (params: CreateRecordParams) => Promise; + updateRecord: (params: UpdateRecordParams) => Promise; } +export type Incident = ExecutorSubActionPushParams['incident']; export type CreateRecordApiParams = ExecutorSubActionCreateRecordParams; export type ExecutorSubActionGetApplicationParams = TypeOf< @@ -75,23 +101,23 @@ export interface CreateRecordApiHandlerArgs extends ExternalServiceApiHandlerArg logger: Logger; } +export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: PushToServiceApiParams; + logger: Logger; +} + export interface GetApplicationHandlerArgs { externalService: ExternalService; } export interface ExternalServiceApi { - createRecord: (args: CreateRecordApiHandlerArgs) => Promise; + createRecord: (args: CreateRecordApiHandlerArgs) => Promise; + pushToService: (args: PushToServiceApiHandlerArgs) => Promise; } -export type SwimlaneExecutorResultData = CreateRecordResponse; +export type SwimlaneExecutorResultData = ExternalServiceIncidentResponse | PushToServiceResponse; export type SwimlaneDataValues = Record; export type SwimlaneDataComments = Record< string, Array<{ fieldId: string; message: string | number; createdDate: string; isRichText: boolean }> >; - -export interface SwimlaneRecordPayload { - applicationId: string; - values?: SwimlaneDataValues; - comments?: SwimlaneDataComments; -} diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx index 2f51d9dddcb93..e9fdd5c2c6d88 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx @@ -16,9 +16,8 @@ const SwimlaneFieldsComponent: React.FunctionComponent< ConnectorFieldsProps > = ({ isEdit = true, fields, connector, onChange }) => { const [state, setState] = useState( - fields ?? { alertSource: null, caseId: null, caseName: null, severity: null } + fields ?? { alertSource: '', caseId: '', caseName: '', severity: '' } ); - const { alertSource, caseId, caseName, severity } = state; const onFieldChange = useCallback((key, { target: { value } }) => { setState((prevState) => ({ @@ -26,59 +25,78 @@ const SwimlaneFieldsComponent: React.FunctionComponent< [key]: value, })); }, []); - const listItems = useMemo( - () => [ - ...(alertSource != null && alertSource.length > 0 - ? [ - { - title: fieldLabels.alertSource, - description: alertSource ?? '', - }, - ] - : []), - ...(caseName != null && caseName.length > 0 - ? [ - { - title: fieldLabels.caseName, - description: caseName ?? '', - }, - ] - : []), - ...(caseId != null && caseId.length > 0 - ? [ - { - title: fieldLabels.caseId, - description: caseId ?? '', - }, - ] - : []), - ...(severity != null && severity.length > 0 - ? [ - { - title: fieldLabels.severity, - description: severity ?? '', - }, - ] - : []), - ], - [alertSource, caseId, caseName, severity] + () => + Object.keys(state).reduce((acc: Array<{ title: string; description: string }>, f) => { + const fieldName = f as keyof SwimlaneUnmappedFieldsType; + return [ + ...acc, + ...(state[fieldName] !== null && state[fieldName] !== '' + ? [ + { + title: fieldLabels[fieldName], + description: state[fieldName] ?? '', + }, + ] + : []), + ]; + }, []), + [state] ); + // const listItems = useMemo( + // () => [ + // ...(alertSource != null && alertSource.length > 0 + // ? [ + // { + // title: fieldLabels.alertSource, + // description: alertSource ?? '', + // }, + // ] + // : []), + // ...(caseName != null && caseName.length > 0 + // ? [ + // { + // title: fieldLabels.caseName, + // description: caseName ?? '', + // }, + // ] + // : []), + // ...(caseId != null && caseId.length > 0 + // ? [ + // { + // title: fieldLabels.caseId, + // description: caseId ?? '', + // }, + // ] + // : []), + // ...(severity != null && severity.length > 0 + // ? [ + // { + // title: fieldLabels.severity, + // description: severity ?? '', + // }, + // ] + // : []), + // ], + // [alertSource, caseId, caseName, severity] + // ); const Fields = useCallback( () => ( - {Object.keys(state).map((f) => ( - <> - - onFieldChange(f, e)} - aria-label="Use aria labels when no actual label is in use" - /> - - - - ))} + {Object.keys(state).map((f) => { + const fieldName = f as keyof SwimlaneUnmappedFieldsType; + return ( + <> + + onFieldChange(f, e)} + /> + + + + ); + })} ), [onFieldChange, state] diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts index 046d9f06112ee..1b7ea8e834e0e 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts @@ -15,11 +15,11 @@ export const ALERT_SOURCE_LABEL = i18n.translate( ); export const CASE_ID_LABEL = i18n.translate('xpack.cases.connectors.swimlane.caseIdLabel', { - defaultMessage: 'Incident Types', + defaultMessage: 'Case Id', }); export const CASE_NAME_LABEL = i18n.translate('xpack.cases.connectors.swimlane.caseNameLabel', { - defaultMessage: 'Incident Types', + defaultMessage: 'Case Name', }); export const SEVERITY_LABEL = i18n.translate('xpack.cases.connectors.swimlane.severityLabel', { diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index 08fa96a3bbe6f..922e13b362501 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -36,7 +36,6 @@ export const get = async ({ try { let theCase: SavedObject; let subCaseIds: string[] = []; - if (ENABLE_CASE_CONNECTOR) { const [caseInfo, subCasesForCaseId] = await Promise.all([ caseService.getCase({ diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 92a9d2910d4a3..08f1920e07f8f 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -215,7 +215,7 @@ export const push = async ({ let updatedComments: SavedObjectsBulkUpdateResponse; const shouldMarkAsClosed = shouldCloseByPush(myCaseConfigure, myCase); - + console.log('step 17', externalService); try { [updatedCase, updatedComments] = await Promise.all([ caseService.patchCase({ diff --git a/x-pack/plugins/cases/server/client/configure/get_default_mappings.ts b/x-pack/plugins/cases/server/client/configure/get_default_mappings.ts index c165f134cfcf4..f6051989d6f26 100644 --- a/x-pack/plugins/cases/server/client/configure/get_default_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/get_default_mappings.ts @@ -5,30 +5,12 @@ * 2.0. */ -import Boom from '@hapi/boom'; - -import { ConnectorTypes, GetDefaultMappingsResponse } from '../../../common'; +import { GetDefaultMappingsResponse } from '../../../common'; import { ConfigureFields } from '../types'; -import { createDefaultMapping, mapSwimlaneFields, SwimlanePublicConfigurationType } from './utils'; +import { createDefaultMapping } from './utils'; export const getDefaultMappings = async ({ - actionsClient, connectorType, - connectorId, }: ConfigureFields): Promise => { - let fields; - if (connectorType === ConnectorTypes.swimlane) { - const results = await actionsClient.get({ - id: connectorId, - }); - - if (results.config && results.config.mappings) { - fields = mapSwimlaneFields( - (results.config.mappings as unknown) as SwimlanePublicConfigurationType['mappings'] - ); - } else { - throw Boom.failedDependency('Something is wrong with the Swimlane connector field mappings.'); - } - } - return createDefaultMapping(connectorType, fields); + return createDefaultMapping(connectorType); }; diff --git a/x-pack/plugins/cases/server/client/configure/utils.ts b/x-pack/plugins/cases/server/client/configure/utils.ts index f31fc8b2f85c1..2454d9f739fcc 100644 --- a/x-pack/plugins/cases/server/client/configure/utils.ts +++ b/x-pack/plugins/cases/server/client/configure/utils.ts @@ -110,18 +110,17 @@ const getPreferredFields = (theType: string, swimlaneMappings?: SwimlaneMappings title = 'short_description'; description = 'description'; comments = 'work_notes'; - } else if (theType === ConnectorTypes.swimlane && swimlaneMappings != null) { - return swimlaneMappings; + } else if (theType === ConnectorTypes.swimlane) { + title = 'alertName'; + description = 'comments'; + comments = 'comments'; } return { title, description, comments }; }; -export const createDefaultMapping = ( - theType: string, - swimlaneMappings?: SwimlaneMappings -): ConnectorMappingsAttributes[] => { - const { description, title, comments } = getPreferredFields(theType, swimlaneMappings); +export const createDefaultMapping = (theType: string): ConnectorMappingsAttributes[] => { + const { description, title, comments } = getPreferredFields(theType); return [ { source: 'title', diff --git a/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts b/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts index 5225e7685c0c8..fd0b67be72e2c 100644 --- a/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts +++ b/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts @@ -6,20 +6,14 @@ */ import { ExternalServiceFormatter } from '../types'; -import { ConnectorSwimlaneTypeFields, SwimlaneFieldsType } from '../../../common'; +import { ConnectorSwimlaneTypeFields, SwimlaneUnmappedFieldsType } from '../../../common'; -const format: ExternalServiceFormatter['format'] = (theCase) => { - const { - alertName = '', - alertSource = null, - caseId = null, - caseName = null, - comments = null, - severity = null, - } = (theCase.connector.fields as ConnectorSwimlaneTypeFields['fields']) ?? {}; - return { alertName, alertSource, caseId, caseName, comments, severity }; +const format: ExternalServiceFormatter['format'] = (theCase) => { + const { alertSource = null, caseId = null, caseName = null, severity = null } = + (theCase.connector.fields as ConnectorSwimlaneTypeFields['fields']) ?? {}; + return { alertSource, caseId, caseName, severity }; }; -export const swimlaneExternalServiceFormatter: ExternalServiceFormatter = { +export const swimlaneExternalServiceFormatter: ExternalServiceFormatter = { format, }; From 6e699b30cc44ca0ad54908829f1dd26ce22bcb86 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 13 May 2021 12:35:52 -0600 Subject: [PATCH 09/96] working well --- .../builtin_action_types/swimlane/api.ts | 29 ++-- .../swimlane/helpers.test.ts | 1 + .../builtin_action_types/swimlane/helpers.ts | 25 ++- .../builtin_action_types/swimlane/index.ts | 5 +- .../builtin_action_types/swimlane/mocks.ts | 14 ++ .../builtin_action_types/swimlane/service.ts | 118 +++++++++++-- .../builtin_action_types/swimlane/types.ts | 42 +++-- .../connectors/swimlane/case_fields.test.tsx | 132 +++------------ .../connectors/swimlane/case_fields.tsx | 157 ++++++++---------- .../components/connectors/swimlane/index.ts | 2 - .../components/connectors/swimlane/types.ts | 9 - .../plugins/cases/server/client/cases/push.ts | 2 +- 12 files changed, 273 insertions(+), 263 deletions(-) delete mode 100644 x-pack/plugins/cases/public/components/connectors/swimlane/types.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts index bf7ae225d9161..ec6850bf1e786 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts @@ -27,7 +27,6 @@ const pushToServiceHandler = async ({ const { comments } = params; let res: ExternalServiceIncidentResponse; const incident: Incident = params.incident; - if (incident.externalId != null) { res = await externalService.updateRecord({ incidentId: incident.externalId, @@ -37,23 +36,17 @@ const pushToServiceHandler = async ({ res = await externalService.createRecord({ incident }); } - console.log('TO DO comments', comments); - // if (comments && Array.isArray(comments) && comments.length > 0) { - // res.comments = []; - // for (const currentComment of comments) { - // const comment = await externalService.createComment({ - // incidentId: res.id, - // comment: currentComment, - // }); - // res.comments = [ - // ...(res.comments ?? []), - // { - // commentId: comment.commentId, - // pushedDate: comment.pushedDate, - // }, - // ]; - // } - // } + const createdDate = new Date().toISOString(); + + if (comments && Array.isArray(comments) && comments.length > 0) { + for (const currentComment of comments) { + await externalService.createComment({ + incidentId: res.id, + comment: currentComment, + createdDate, + }); + } + } return res; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts index 002331a1f32ce..9cad8461528dc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts @@ -61,6 +61,7 @@ describe('Create Record Mapping', () => { caseName: 'Case Name', caseId: 'es3456789', comments: 'This is a comment', + externalId: null, }; const data = getBodyForEventAction(appId, mappingConfig, params); expect(data?.values?.[mappingConfig.alertSourceConfig.id]).toEqual(params.alertSource); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts index 93c0cedf87fb0..6d761e8d8a277 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts @@ -7,19 +7,22 @@ import { CreateRecordParams, + ExecutorSubActionCreateRecordParams, MappingConfigType, - SwimlaneRecordPayload, SwimlaneDataComments, SwimlaneDataValues, + SwimlaneRecordPayload, } from './types'; export const getBodyForEventAction = ( applicationId: string, mappingConfig: MappingConfigType, - params: CreateRecordParams['incident'] + params: CreateRecordParams['incident'], + incidentId?: string ): SwimlaneRecordPayload => { const data: SwimlaneRecordPayload = { applicationId, + ...(incidentId ? { id: incidentId } : {}), }; const values: SwimlaneDataValues = {}; @@ -38,7 +41,10 @@ export const getBodyForEventAction = ( const createdDate = new Date().toISOString(); const { id, fieldType } = fieldMap; - const paramName = mappingsKey.replace('Config', ''); + const paramName = mappingsKey.replace( + 'Config', + '' + ) as keyof ExecutorSubActionCreateRecordParams; if (params[paramName]) { const value = params[paramName]; if (value) { @@ -74,3 +80,16 @@ export const getBodyForEventAction = ( return data; }; + +export const removeCommentFieldUpdatedInformation = (content: string): string => { + // these values are added on in `transformFields` in `x-pack/plugins/cases/server/client/cases/utils.ts` + // have to remove to compare string values + // probably a bug + if (content.indexOf(` (updated at `) > 0) { + return content.slice(0, content.indexOf(` (updated at`)); + } + if (content.indexOf(` (created at `) > 0) { + return content.slice(0, content.indexOf(` (created at`)); + } + return content; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts index 1c742bd5a4718..f93329fb50e03 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts @@ -119,11 +119,8 @@ async function executor( externalService, params: pushToServiceParams, logger, + mappings: config.mappings, }); - data = { - ...data, - url: `${config.apiUrl}/record/${config.appId}/${data.id}`, - }; logger.debug(`response push to service for incident id: ${data.id}`); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts index 8fbbd3edaaa30..7990bd6c80335 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts @@ -13,9 +13,23 @@ import { const createMock = (): jest.Mocked => { return { + createComment: jest.fn().mockImplementation(() => + Promise.resolve({ + pushedDate: '123456', + }) + ), createRecord: jest.fn().mockImplementation(() => Promise.resolve({ id: '123456', + title: 'neato', + url: 'swimlane.com', + }) + ), + updateRecord: jest.fn().mockImplementation(() => + Promise.resolve({ + id: '123456', + title: 'neato', + url: 'swimlane.com', }) ), }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index c941b5a0d6414..01caa50b29163 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -10,15 +10,19 @@ import axios from 'axios'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { getErrorMessage, request } from '../lib/axios_utils'; -import { getBodyForEventAction } from './helpers'; +import { getBodyForEventAction, removeCommentFieldUpdatedInformation } from './helpers'; import { - SwimlanePublicConfigurationType, + CreateCommentParams, + CreateRecordParams, ExternalService, ExternalServiceCredentials, - CreateRecordParams, ExternalServiceIncidentResponse, - SwimlaneSecretConfigurationType, MappingConfigType, + SwimlaneComment, + SwimlanePublicConfigurationType, + SwimlaneRecordPayload, + SwimlaneSecretConfigurationType, + UpdateRecordParams, } from './types'; import * as i18n from './translations'; @@ -45,9 +49,16 @@ export const createExternalService = ( const apiUrl = urlWithoutTrailingSlash.endsWith('api') ? urlWithoutTrailingSlash : urlWithoutTrailingSlash + '/api'; - const recordUrl = `${apiUrl}/app/{appId}/record`; - const getPostRecordUrl = (id: string) => recordUrl.replace('{appId}', id); + const getPostRecordUrl = (id: string) => `${apiUrl}/app/${id}/record`; + const getPostRecordIdUrl = (id: string, recordId: string) => + `${getPostRecordUrl(id)}/${recordId}`; + const getRecordIdUrl = (id: string, recordId: string) => + `${urlWithoutTrailingSlash}/record/${id}/${recordId}`; + const getPostCommentUrl = (id: string, recordId: string, commentFieldId: string) => + `${getPostRecordIdUrl(id, recordId)}/${commentFieldId}/comment`; + const getCommentFieldId = (fieldMappings: MappingConfigType): string | null => + fieldMappings.commentsConfig?.id || null; const createRecord = async ( params: CreateRecordParams ): Promise => { @@ -56,14 +67,14 @@ export const createExternalService = ( const data = getBodyForEventAction(appId, mappingConfig, params.incident); const res = await request({ axios: axiosInstance, - url: getPostRecordUrl(appId), - logger, configurationUtilities, + data, headers, + logger, method: 'post', - data, + url: getPostRecordUrl(appId), }); - return { id: res.data.id, title: res.data.name }; + return { id: res.data.id, title: res.data.name, url: getRecordIdUrl(appId, res.data.id) }; } catch (error) { throw new Error( getErrorMessage( @@ -75,32 +86,105 @@ export const createExternalService = ( }; const updateRecord = async ( - params: CreateRecordParams + params: UpdateRecordParams ): Promise => { try { const mappingConfig = mappings as MappingConfigType; - const data = getBodyForEventAction(appId, mappingConfig, params.incident); - const res = await request({ + const data = getBodyForEventAction(appId, mappingConfig, params.incident, params.incidentId); + const res = await request({ axios: axiosInstance, - url: getPostRecordUrl(appId), + configurationUtilities, + data, + headers, logger, + method: 'patch', + url: getPostRecordIdUrl(appId, params.incidentId), + }); + const fieldId = mappingConfig.commentsConfig?.id; + let potentialNewDescription: SwimlaneComment[] = []; + let isDescriptionPosted = true; + if ( + fieldId != null && + res.data.comments[fieldId].length && + data.comments != null && + data.comments[fieldId].length === 1 + ) { + // this is the description, it is sent as a comment. + // on update, it needs a separate comment post + // will only ever be length of 1 + potentialNewDescription = data.comments[fieldId]; + // remove update time to compare string only + const messageString = removeCommentFieldUpdatedInformation( + `${potentialNewDescription[0].message}` + ); + + // already saved description/comments + const existingComments: SwimlaneComment[] = res.data.comments[fieldId]; + + // check if description is updated + isDescriptionPosted = existingComments.some( + ({ message }) => removeCommentFieldUpdatedInformation(`${message}`) === messageString + ); + if (!isDescriptionPosted) { + // if description has updated + // post as comments + await createComment({ + incidentId: params.incidentId, + comment: { comment: potentialNewDescription[0].message }, + createdDate: potentialNewDescription[0].createdDate, + }); + } + } + return { + id: res.data.id, + title: res.data.name, + url: getRecordIdUrl(appId, params.incidentId), + }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to update record in application with id ${appId}. Error: ${error.message}` + ) + ); + } + }; + + const createComment = async ({ incidentId, comment, createdDate }: CreateCommentParams) => { + try { + const mappingConfig = mappings as MappingConfigType; + const fieldId = getCommentFieldId(mappingConfig); + if (fieldId == null) { + throw new Error(`No comment field mapped in ${i18n.NAME} connector`); + } + const data = { + createdDate, + fieldId, + isRichText: true, + message: comment.comment, + }; + await request({ + axios: axiosInstance, configurationUtilities, + data, headers, + logger, method: 'post', - data, + url: getPostCommentUrl(appId, incidentId, fieldId), }); - return { id: res.data.id, title: res.data.name }; + return { pushedDate: createdDate }; } catch (error) { throw new Error( getErrorMessage( i18n.NAME, - `Unable to create record in application with id ${appId}. Error: ${error.message}` + `Unable to create comment in application with id ${appId}. Error: ${error.message}` ) ); } }; return { + createComment, createRecord, updateRecord, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts index f1bb466468e49..10e33af0ec64a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts @@ -6,7 +6,6 @@ */ /* eslint-disable @typescript-eslint/no-explicit-any */ - import { TypeOf } from '@kbn/config-schema'; import { Logger } from '@kbn/logging'; import { @@ -54,11 +53,16 @@ export type PushToServiceApiParams = ExecutorSubActionPushParams; export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { params: PushToServiceApiParams; logger: Logger; + mappings: MappingConfigType; } export interface ExternalServiceIncidentResponse { id: string; title: string; + url: string; +} +export interface ExternalServiceCommentResponse { + pushedDate: string; } export interface FieldConfig { @@ -70,16 +74,13 @@ export interface FieldConfig { export interface SwimlaneRecordPayload { applicationId: string; + id?: string; values?: SwimlaneDataValues; comments?: SwimlaneDataComments; } -export interface PushToServiceResponse extends ExternalServiceIncidentResponse { - url: string; -} - -// export type ExternalServiceParams = Record; export interface ExternalService { + createComment: (params: CreateCommentParams) => Promise; createRecord: (params: CreateRecordParams) => Promise; updateRecord: (params: UpdateRecordParams) => Promise; } @@ -101,11 +102,6 @@ export interface CreateRecordApiHandlerArgs extends ExternalServiceApiHandlerArg logger: Logger; } -export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { - params: PushToServiceApiParams; - logger: Logger; -} - export interface GetApplicationHandlerArgs { externalService: ExternalService; } @@ -115,9 +111,23 @@ export interface ExternalServiceApi { pushToService: (args: PushToServiceApiHandlerArgs) => Promise; } -export type SwimlaneExecutorResultData = ExternalServiceIncidentResponse | PushToServiceResponse; +export type SwimlaneExecutorResultData = ExternalServiceIncidentResponse; export type SwimlaneDataValues = Record; -export type SwimlaneDataComments = Record< - string, - Array<{ fieldId: string; message: string | number; createdDate: string; isRichText: boolean }> ->; +export interface SwimlaneComment { + fieldId: string; + message: string | number; + createdDate: string; + isRichText: boolean; +} +export type SwimlaneDataComments = Record; + +export interface SimpleComment { + comment: SwimlaneComment['message']; + commentId?: string; +} + +export interface CreateCommentParams { + incidentId: string; + comment: SimpleComment; + createdDate: string; +} diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx index dda6ba5de95cc..cd11e27d61747 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx @@ -7,128 +7,44 @@ import React from 'react'; import { mount } from 'enzyme'; -import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { waitFor } from '@testing-library/react'; import { connector } from '../mock'; -import { useGetIncidentTypes } from './use_get_incident_types'; -import { useGetSeverity } from './use_get_severity'; import Fields from './case_fields'; jest.mock('../../../common/lib/kibana'); -jest.mock('./use_get_incident_types'); -jest.mock('./use_get_severity'); - -const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; -const useGetSeverityMock = useGetSeverity as jest.Mock; - -describe('ResilientParamsFields renders', () => { - const useGetIncidentTypesResponse = { - isLoading: false, - incidentTypes: [ - { - id: 19, - name: 'Malware', - }, - { - id: 21, - name: 'Denial of Service', - }, - ], - }; - - const useGetSeverityResponse = { - isLoading: false, - severity: [ - { - id: 4, - name: 'Low', - }, - { - id: 5, - name: 'Medium', - }, - { - id: 6, - name: 'High', - }, - ], - }; +describe('SwimlaneParamsFields renders', () => { const fields = { - severityCode: '6', - incidentTypes: ['19'], + alertSource: '1', + caseId: '2', + caseName: '3', + severity: '4', }; const onChange = jest.fn(); - beforeEach(() => { - useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse); - useGetSeverityMock.mockReturnValue(useGetSeverityResponse); jest.clearAllMocks(); }); - test('all params fields are rendered', () => { - const wrapper = mount(); - expect(wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('options')).toEqual( - [ - { label: 'Malware', value: '19' }, - { label: 'Denial of Service', value: '21' }, - ] - ); - - expect( - wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('selectedOptions') - ).toEqual([{ label: 'Malware', value: '19' }]); - - expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('value')).toStrictEqual( - '6' - ); - }); - - test('it disabled the fields when loading incident types', () => { - useGetIncidentTypesMock.mockReturnValue({ ...useGetIncidentTypesResponse, isLoading: true }); - - const wrapper = mount(); - - expect( - wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('isDisabled') - ).toBeTruthy(); - }); - - test('it disabled the fields when loading severity', () => { - useGetSeverityMock.mockReturnValue({ - ...useGetSeverityResponse, - isLoading: true, - }); - - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('disabled')).toBeTruthy(); - }); - - test('it sets issue type correctly', async () => { - const wrapper = mount(); - - await waitFor(() => { - ((wrapper.find(EuiComboBox).props() as unknown) as { - onChange: (a: EuiComboBoxOptionOption[]) => void; - }).onChange([{ value: '19', label: 'Denial of Service' }]); - }); - - expect(onChange).toHaveBeenCalledWith({ incidentTypes: ['19'], severityCode: '6' }); - }); - - test('it sets severity correctly', async () => { - const wrapper = mount(); - - wrapper - .find('select[data-test-subj="severitySelect"]') - .first() - .simulate('change', { - target: { value: '4' }, + Object.entries(fields).forEach(([k, v]) => + describe(`${k} tests`, () => { + test(`param field is rendered`, () => { + const wrapper = mount(); + expect(wrapper.find(`[data-test-subj="${k}"]`).first().prop('value')).toStrictEqual(v); }); - expect(onChange).toHaveBeenCalledWith({ incidentTypes: ['19'], severityCode: '4' }); - }); + test(`onChange is called`, () => { + const wrapper = mount(); + const newValue = 'wowie'; + wrapper + .find(`input[data-test-subj="${k}"]`) + .first() + .simulate('change', { + target: { value: newValue }, + }); + + expect(onChange).toHaveBeenCalledWith({ ...fields, [k]: newValue }); + }); + }) + ); }); diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx index e9fdd5c2c6d88..676d16a767bbf 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo, useCallback, useState } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { EuiFieldText, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { ConnectorFieldsProps } from '../types'; @@ -15,95 +15,82 @@ import { fieldLabels } from './index'; const SwimlaneFieldsComponent: React.FunctionComponent< ConnectorFieldsProps > = ({ isEdit = true, fields, connector, onChange }) => { - const [state, setState] = useState( - fields ?? { alertSource: '', caseId: '', caseName: '', severity: '' } - ); + const { alertSource, caseId, caseName, severity } = fields || { + alertSource: null, + caseId: null, + caseName: null, + severity: null, + }; - const onFieldChange = useCallback((key, { target: { value } }) => { - setState((prevState) => ({ - ...prevState, - [key]: value, - })); - }, []); + const onFieldChange = useCallback( + (key, value) => { + onChange({ + ...fields, + alertSource, + caseId, + caseName, + severity, + [key]: value, + }); + }, + [alertSource, caseId, caseName, fields, onChange, severity] + ); const listItems = useMemo( () => - Object.keys(state).reduce((acc: Array<{ title: string; description: string }>, f) => { - const fieldName = f as keyof SwimlaneUnmappedFieldsType; - return [ - ...acc, - ...(state[fieldName] !== null && state[fieldName] !== '' - ? [ - { - title: fieldLabels[fieldName], - description: state[fieldName] ?? '', - }, - ] - : []), - ]; - }, []), - [state] + Object.entries({ alertSource, caseId, caseName, severity }).reduce( + (acc: Array<{ title: string; description: string }>, [key, value]) => { + const fieldName = key as keyof SwimlaneUnmappedFieldsType; + return [ + ...acc, + ...(value !== null && value !== '' + ? [ + { + title: fieldLabels[fieldName], + description: value ?? '', + }, + ] + : []), + ]; + }, + [] + ), + [alertSource, caseId, caseName, severity] ); - // const listItems = useMemo( - // () => [ - // ...(alertSource != null && alertSource.length > 0 - // ? [ - // { - // title: fieldLabels.alertSource, - // description: alertSource ?? '', - // }, - // ] - // : []), - // ...(caseName != null && caseName.length > 0 - // ? [ - // { - // title: fieldLabels.caseName, - // description: caseName ?? '', - // }, - // ] - // : []), - // ...(caseId != null && caseId.length > 0 - // ? [ - // { - // title: fieldLabels.caseId, - // description: caseId ?? '', - // }, - // ] - // : []), - // ...(severity != null && severity.length > 0 - // ? [ - // { - // title: fieldLabels.severity, - // description: severity ?? '', - // }, - // ] - // : []), - // ], - // [alertSource, caseId, caseName, severity] - // ); - const Fields = useCallback( - () => ( - - {Object.keys(state).map((f) => { - const fieldName = f as keyof SwimlaneUnmappedFieldsType; - return ( - <> - - onFieldChange(f, e)} - /> - - - - ); - })} - - ), - [onFieldChange, state] - ); - return isEdit ? ( - + + + onFieldChange('alertSource', e.target.value)} + /> + + + + onFieldChange('caseId', e.target.value)} + /> + + + + onFieldChange('caseName', e.target.value)} + /> + + + + onFieldChange('severity', e.target.value)} + /> + + + ) : ( => { return { id: ConnectorTypes.swimlane, diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/types.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/types.ts deleted file mode 100644 index 06506d2c0d2f9..0000000000000 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export type ResilientIncidentTypes = Array<{ id: number; name: string }>; -export type ResilientSeverity = ResilientIncidentTypes; diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 08f1920e07f8f..92a9d2910d4a3 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -215,7 +215,7 @@ export const push = async ({ let updatedComments: SavedObjectsBulkUpdateResponse; const shouldMarkAsClosed = shouldCloseByPush(myCaseConfigure, myCase); - console.log('step 17', externalService); + try { [updatedCase, updatedComments] = await Promise.all([ caseService.patchCase({ From 1acf2fa20d0ba5b7832e8732c0e27c241c8f1d01 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 13 May 2021 13:06:01 -0600 Subject: [PATCH 10/96] fixed jest --- .../builtin_action_types/swimlane/api.test.ts | 66 +++++++++++++++-- .../builtin_action_types/swimlane/index.ts | 1 - .../builtin_action_types/swimlane/mocks.ts | 74 ++++++++++++++----- .../swimlane/service.test.ts | 40 +--------- .../builtin_action_types/swimlane/types.ts | 1 - .../swimlane/swimlane.test.tsx | 20 ++--- 6 files changed, 122 insertions(+), 80 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts index 17e7e1096c470..b36e2c1a06849 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts @@ -7,10 +7,17 @@ import { api } from './api'; import { ExternalService } from './types'; -import { externalServiceMock } from './mocks'; +import { externalServiceMock, recordResponseCreate, recordResponseUpdate } from './mocks'; import { Logger } from '@kbn/logging'; let mockedLogger: jest.Mocked; - +const params = { + alertName: 'alert name', + caseName: 'case name', + severity: 'critical', + alertSource: 'elastic', + caseId: '123456', + comments: 'some comments', +}; describe('api', () => { let externalService: jest.Mocked; @@ -19,7 +26,7 @@ describe('api', () => { }); describe('createRecord', () => { - test('it creates a record correctly', async () => { + test('it creates a record correctly with a comment', async () => { const res = await api.createRecord({ externalService, logger: mockedLogger, @@ -32,9 +39,58 @@ describe('api', () => { comments: 'some comments', }, }); - expect(res).toEqual({ - id: '123456', + expect(res).toEqual(recordResponseCreate); + }); + }); + + describe('pushToService', () => { + test('it pushes a new record', async () => { + const res = await api.pushToService({ + externalService, + logger: mockedLogger, + params: { + incident: { + ...params, + externalId: null, + }, + comments: [], + }, + }); + expect(externalService.createComment).not.toHaveBeenCalled(); + expect(externalService.createRecord).toHaveBeenCalled(); + expect(externalService.updateRecord).not.toHaveBeenCalled(); + expect(res).toEqual(recordResponseCreate); + }); + test('it pushes a new record with a comment', async () => { + await api.pushToService({ + externalService, + logger: mockedLogger, + params: { + incident: { + ...params, + externalId: null, + }, + comments: [{ comment: 'some comments', commentId: '123' }], + }, + }); + expect(externalService.createComment).toHaveBeenCalled(); + }); + test('updates existing record', async () => { + const res = await api.pushToService({ + externalService, + logger: mockedLogger, + params: { + incident: { + ...params, + externalId: '1234', + }, + comments: [{ comment: 'some comments', commentId: '123' }], + }, }); + expect(externalService.createComment).toHaveBeenCalled(); + expect(externalService.createRecord).not.toHaveBeenCalled(); + expect(externalService.updateRecord).toHaveBeenCalled(); + expect(res).toEqual(recordResponseUpdate); }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts index f93329fb50e03..2730b6cdf3baf 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts @@ -119,7 +119,6 @@ async function executor( externalService, params: pushToServiceParams, logger, - mappings: config.mappings, }); logger.debug(`response push to service for incident id: ${data.id}`); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts index 7990bd6c80335..81e8ac4f2302a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts @@ -11,27 +11,24 @@ import { ExternalService, } from './types'; +export const recordResponseCreate = { + id: '123456', + title: 'neato', + url: 'swimlane.com', +}; +export const recordResponseUpdate = { + id: '98765', + title: 'not neato', + url: 'laneswim.com', +}; +export const commentResponse = { + id: '123456', +}; const createMock = (): jest.Mocked => { return { - createComment: jest.fn().mockImplementation(() => - Promise.resolve({ - pushedDate: '123456', - }) - ), - createRecord: jest.fn().mockImplementation(() => - Promise.resolve({ - id: '123456', - title: 'neato', - url: 'swimlane.com', - }) - ), - updateRecord: jest.fn().mockImplementation(() => - Promise.resolve({ - id: '123456', - title: 'neato', - url: 'swimlane.com', - }) - ), + createComment: jest.fn().mockImplementation(() => Promise.resolve(commentResponse)), + createRecord: jest.fn().mockImplementation(() => Promise.resolve(recordResponseCreate)), + updateRecord: jest.fn().mockImplementation(() => Promise.resolve(recordResponseUpdate)), }; }; @@ -39,6 +36,45 @@ const externalServiceMock = { create: createMock, }; +export const mappings = { + alertSourceConfig: { + id: 'adnjls', + name: 'Alert Source', + key: 'alert-source', + fieldType: 'text', + }, + severityConfig: { + id: 'adnlas', + name: 'Severity', + key: 'severity', + fieldType: 'text', + }, + alertNameConfig: { + id: 'adnfls', + name: 'Alert Name', + key: 'alert-name', + fieldType: 'text', + }, + caseIdConfig: { + id: 'a6sst', + name: 'Case Id', + key: 'case-id-name', + fieldType: 'text', + }, + caseNameConfig: { + id: 'a6fst', + name: 'Case Name', + key: 'case-name', + fieldType: 'text', + }, + commentsConfig: { + id: 'a6fdf', + name: 'Comments', + key: 'comments', + fieldType: 'text', + }, +}; + const executorParams: ExecutorSubActionCreateRecordParams = { alertName: 'alert-name', alertSource: 'alert-source', diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts index e4b8a0a90f11f..5f16e8eeb1c64 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts @@ -11,6 +11,7 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { Logger } from '../../../../../../src/core/server'; import { actionsConfigMock } from '../../actions_config.mock'; import { createExternalService } from './service'; +import { mappings } from './mocks'; const logger = loggingSystemMock.create().get() as jest.Mocked; @@ -26,45 +27,6 @@ jest.mock('../lib/axios_utils', () => { axios.create = jest.fn(() => axios); const configurationUtilities = actionsConfigMock.create(); -const mappings = { - alertSourceConfig: { - id: 'adnjls', - name: 'Alert Source', - key: 'alert-source', - fieldType: 'text', - }, - severityConfig: { - id: 'adnlas', - name: 'Severity', - key: 'severity', - fieldType: 'text', - }, - alertNameConfig: { - id: 'adnfls', - name: 'Alert Name', - key: 'alert-name', - fieldType: 'text', - }, - caseIdConfig: { - id: 'a6sst', - name: 'Case Id', - key: 'case-id-name', - fieldType: 'text', - }, - caseNameConfig: { - id: 'a6fst', - name: 'Case Name', - key: 'case-name', - fieldType: 'text', - }, - commentsConfig: { - id: 'a6fdf', - name: 'Comments', - key: 'comments', - fieldType: 'text', - }, -}; - describe('Swimlane Service', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts index 10e33af0ec64a..3ee192ead4443 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts @@ -53,7 +53,6 @@ export type PushToServiceApiParams = ExecutorSubActionPushParams; export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { params: PushToServiceApiParams; logger: Logger; - mappings: MappingConfigType; } export interface ExternalServiceIncidentResponse { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx index ddfa5b23234a7..a1de00ae117fc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx @@ -46,26 +46,17 @@ describe('swimlane connector validation', () => { }, }, } as SwimlaneActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - apiToken: [], - apiUrl: [], - appId: [], - mappings: [], - }, + config: { errors: { apiUrl: [], appId: [], mappings: [] } }, + secrets: { errors: { apiToken: [] } }, }); // @ts-ignore delete actionConnector.config.apiUrl; actionConnector.secrets.apiToken = 'test1'; expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - errors: { - apiToken: [], - apiUrl: [], - appId: [], - mappings: [], - }, + config: { errors: { apiUrl: ['URL is required.'], appId: [], mappings: [] } }, + secrets: { errors: { apiToken: [] } }, }); }); @@ -83,9 +74,8 @@ describe('swimlane connector validation', () => { }, } as SwimlaneActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(actionTypeModel.validateConnector(actionConnector).config).toEqual({ errors: { - apiToken: [], apiUrl: [], appId: [], mappings: ['Field mappings are required.'], From 19a2da7897204684765339cb24214e1fa609c08f Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 13 May 2021 13:29:36 -0600 Subject: [PATCH 11/96] fixing --- .../actions/server/builtin_action_types/swimlane/service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index 01caa50b29163..587a0e9356e9a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -105,8 +105,10 @@ export const createExternalService = ( let isDescriptionPosted = true; if ( fieldId != null && + res.data.comments[fieldId] != null && res.data.comments[fieldId].length && data.comments != null && + data.comments[fieldId] != null && data.comments[fieldId].length === 1 ) { // this is the description, it is sent as a comment. From e92dd16262b442393784c95aa917f98d037bdcd6 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 13 May 2021 14:06:49 -0600 Subject: [PATCH 12/96] made connector better --- .../swimlane/steps/swimlane_fields.tsx | 2 - .../swimlane/swimlane_connectors.tsx | 86 ++++++++++++------- 2 files changed, 56 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index 200c8eb4a0d44..3bc0b50a7db95 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -43,8 +43,6 @@ export const SwimlaneFields: React.FunctionComponent = ({ }; const resetConnection = () => { - // reset fields - // setConnectionStatus('incomplete'); updateCurrentStep(1); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx index 3918406d8b7a1..6bf2bae37765c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, useState } from 'react'; +import React, { Fragment, useCallback, useMemo, useState } from 'react'; import { EuiForm, EuiSpacer, EuiStepsHorizontal, EuiStepStatus } from '@elastic/eui'; import * as i18n from './translations'; import { ActionConnectorFieldsProps } from '../../../../types'; @@ -16,38 +16,64 @@ const SwimlaneActionConnectorFields: React.FunctionComponent< ActionConnectorFieldsProps > = ({ errors, action, editActionConfig, editActionSecrets, readOnly }) => { const [currentStep, setCurrentStep] = useState(1); - const stepMap: { [key: number]: any } = { - 1: SwimlaneConnection, - 2: SwimlaneFields, - }; - const CurrentStepForm = stepMap[currentStep]; - const [connectionStatus] = useState('incomplete' as EuiStepStatus); - const [fieldsConfigured] = useState('incomplete' as EuiStepStatus); - const [fields, setFields] = useState(new Array()); - - const updateCurrentStep = (step: number) => { - setCurrentStep(step); - }; + const stepMap = useMemo<{ [key: number]: any }>( + () => ({ + 1: SwimlaneConnection, + 2: SwimlaneFields, + }), + [] + ); + const CurrentStepForm = useMemo(() => { + return stepMap[currentStep]; + }, [currentStep, stepMap]); + const [connectionStatus, setConnectionStatus] = useState('incomplete'); + const [fieldsConfigured, setFieldsConfigured] = useState('incomplete'); - const updateFields = (items: SwimlaneFieldMappingConfig[]) => { - setFields(items); - }; + const [fields, setFields] = useState([]); - const setupSteps = [ - { - title: i18n.SW_CONFIGURE_CONNECTION_LABEL, - status: connectionStatus, - onClick: () => {}, + const updateCurrentStep = useCallback( + (step: number) => { + setCurrentStep(step); + if (step === 2) { + setConnectionStatus('complete'); + } else if (step === 1) { + setConnectionStatus('incomplete'); + setFieldsConfigured('incomplete'); + editActionConfig('mappings', {}); + } }, - { - title: i18n.SW_MAPPING_TITLE_TEXT_FIELD_LABEL, - disabled: connectionStatus !== 'complete', - status: fieldsConfigured, - onClick: () => {}, - }, - ]; + [editActionConfig] + ); + const setupSteps = useMemo( + () => [ + { + title: i18n.SW_CONFIGURE_CONNECTION_LABEL, + status: connectionStatus, + onClick: () => {}, + }, + { + title: i18n.SW_MAPPING_TITLE_TEXT_FIELD_LABEL, + disabled: connectionStatus !== 'complete', + status: fieldsConfigured, + onClick: () => {}, + }, + ], + [connectionStatus, fieldsConfigured] + ); + + const editActionConfigCb = useCallback( + (k: string, v: string) => { + editActionConfig(k, v); + if (k === 'mappings' && Object.keys(v).length === 6) { + setFieldsConfigured('complete'); + } else if (fieldsConfigured === 'complete') { + setFieldsConfigured('incomplete'); + } + }, + [editActionConfig, fieldsConfigured] + ); return ( @@ -55,12 +81,12 @@ const SwimlaneActionConnectorFields: React.FunctionComponent< From 95953507ab6dd82ef871fc875f214a9b430b4458 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 14 May 2021 12:07:55 -0600 Subject: [PATCH 13/96] fix and add func tests --- x-pack/plugins/actions/README.md | 43 ++ .../builtin_action_types/swimlane/service.ts | 2 + .../swimlane/translations.ts | 8 +- .../actions/builtin_action_types/swimlane.ts | 74 ++- .../actions_simulators/server/plugin.ts | 1 + .../server/swimlane_simulation.ts | 33 +- .../actions/builtin_action_types/swimlane.ts | 595 +++++++++++++----- 7 files changed, 568 insertions(+), 188 deletions(-) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 5b4a197eea462..08c76e744e433 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -54,6 +54,10 @@ Table of Contents - [`subActionParams (getFields)`](#subactionparams-getfields-2) - [`subActionParams (incidentTypes)`](#subactionparams-incidenttypes) - [`subActionParams (severity)`](#subactionparams-severity) + - [Swimlane](#swimlane) + - [`params`](#params-3) + - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-3) + - [`subActionParams (createRecord)`](#subactionparams-createRecord) - [Command Line Utility](#command-line-utility) - [Developing New Action Types](#developing-new-action-types) - [licensing](#licensing) @@ -394,6 +398,45 @@ No parameters for the `incidentTypes` subaction. Provide an empty object `{}`. No parameters for the `severity` subaction. Provide an empty object `{}`. +--- +## Swimlane + + +### `params` + +| Property | Description | Type | +| --------------- | -------------------------------------------------------------------------------------------------- | ------ | +| subAction | The subaction to perform. It can be `pushToService` or `createRecord`. | string | +| subActionParams | The parameters of the subaction. | object | + +#### `subActionParams (createRecord)` + +| Property | Description | Type | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| alertName | The alert name of the incident. | string | +| alertSource | The alert source of the incident. | string _(optional)_ | +| caseId | The case id of the incident. | string _(optional)_ | +| caseName | The case name of the incident. | string _(optional)_ | +| comments | The comments of the incident. | string _(optional)_ | +| severity | The severity of the incident. | string _(optional)_ | + +`subActionParams (pushToService)` + +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- | +| incident | The Swimlane incident. | object | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ | + +The following table describes the properties of the `incident` object. + +| Property | Description | Type | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| alertName | The alert name of the incident. | string | +| alertSource | The alert source of the incident. | string _(optional)_ | +| caseId | The case id of the incident. | string _(optional)_ | +| caseName | The case name of the incident. | string _(optional)_ | +| comments | The comments of the incident. | string _(optional)_ | +| severity | The severity of the incident. --- # Command Line Utility diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index 587a0e9356e9a..15293456bd624 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -43,6 +43,7 @@ export const createExternalService = ( const headers: Record = { 'Content-Type': 'application/json', 'Private-Token': `${secrets.apiToken}`, + 'kbn-xsrf': 'why', }; const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url; @@ -105,6 +106,7 @@ export const createExternalService = ( let isDescriptionPosted = true; if ( fieldId != null && + res.data.comments != null && res.data.comments[fieldId] != null && res.data.comments[fieldId].length && data.comments != null && diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts index 43f3f264c651e..61dee9e031ccd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts @@ -24,19 +24,19 @@ export const SW_ACTION_TYPE_TITLE = i18n.translate( export const SW_REQUIRED_ALERT_NAME = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertName', { - defaultMessage: 'AlertName is required.', + defaultMessage: 'Alert Name is required.', } ); export const SW_REQUIRED_APP_ID_TEXT = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAppIdText', { - defaultMessage: 'An AppId is required.', + defaultMessage: 'An App Id is required.', } ); export const SW_REQUIRED_FIELD_MAPPINGS_TEXT = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAppIdText', + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredFieldMappingsText', { defaultMessage: 'Field mappings are required.', } @@ -61,7 +61,7 @@ export const SW_GET_APPLICATION_API_ERROR = (id: string | null) => export const SW_API_URL_TEXT_FIELD_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiUrlTextFieldLabel', { - defaultMessage: 'API URL', + defaultMessage: 'API Url', } ); diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts index 8bc8c20d9a195..d181f5dbd8b00 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts @@ -6,26 +6,82 @@ */ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function swimlaneTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const mockSwimlane = { + name: 'A swimlane action', + actionTypeId: '.swimlane', + config: { + apiUrl: 'http://swimlane.mynonexistent.co', + appId: '123456asdf', + mappings: { + alertSourceConfig: { + id: 'adnjls', + name: 'Alert Source', + key: 'alert-source', + fieldType: 'text', + }, + severityConfig: { + id: 'adnlas', + name: 'Severity', + key: 'severity', + fieldType: 'text', + }, + alertNameConfig: { + id: 'adnfls', + name: 'Alert Name', + key: 'alert-name', + fieldType: 'text', + }, + caseIdConfig: { + id: 'a6sst', + name: 'Case Id', + key: 'case-id-name', + fieldType: 'text', + }, + caseNameConfig: { + id: 'a6fst', + name: 'Case Name', + key: 'case-name', + fieldType: 'text', + }, + commentsConfig: { + id: 'a6fdf', + name: 'Comments', + key: 'comments', + fieldType: 'text', + }, + }, + }, + secrets: { + apiToken: 'swimlane-api-key', + }, + }; + describe('swimlane', () => { + let swimlaneSimulatorURL: string = ''; - describe('swimlane action', () => { + // need to wait for kibanaServer to settle ... + before(() => { + swimlaneSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SWIMLANE) + ); + }); it('should return 403 when creating a swimlane action', async () => { await supertest .post('/api/actions/action') .set('kbn-xsrf', 'foo') .send({ - name: 'A swimlane action', - actionTypeId: '.swimlane', + ...mockSwimlane, config: { - apiUrl: 'http://localhost', - appId: '123456asdf', - username: 'username', - }, - secrets: { - apiToken: 'swimlane-api-key', + ...mockSwimlane.config, + apiUrl: swimlaneSimulatorURL, }, }) .expect(403, { diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts index a1fb02285ef5c..bd619b9ed9345 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts @@ -51,6 +51,7 @@ export function getAllExternalServiceSimulatorPaths(): string[] { allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.JIRA}/rest/api/2/issue`); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.JIRA}/rest/api/2/createmeta`); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.RESILIENT}/rest/orgs/201/incidents`); + allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SWIMLANE}/api`); return allPaths; } diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.ts index 590df86d374ca..bf673ba843e1c 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.ts @@ -16,7 +16,7 @@ import { export function initPlugin(router: IRouter, path: string) { router.post( { - path, + path: `${path}/api/app/{id}/record`, options: { authRequired: false, }, @@ -30,12 +30,31 @@ export function initPlugin(router: IRouter, path: string) { req: KibanaRequest, res: KibanaResponseFactory ): Promise> { - const { body } = req; - - return jsonResponse(res, 202, { - status: 'success', - message: 'Event processed', - ...(body.alertName ? { alertName: body.alertName } : {}), + return jsonResponse(res, 200, { + id: 'wowzeronza', + name: 'ET-69', + }); + } + ); + router.patch( + { + path: `${path}/api/app/{id}/record/{recordId}`, + options: { + authRequired: false, + }, + validate: {}, + }, + // Swimlane simulator: create an action pointing here, and you can get + // different responses based on the message posted. See the README.md for + // more info. + async function ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + return jsonResponse(res, 200, { + id: 'wowzeronza', + name: 'ET-69', }); } ); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts index 4268f2993a028..455db46a02535 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts @@ -22,206 +22,465 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const configService = getService('config'); - describe('swimlane action', () => { - let simulatedActionId = ''; + const mockSwimlane = { + name: 'A swimlane action', + actionTypeId: '.swimlane', + config: { + apiUrl: 'http://swimlane.mynonexistent.com', + appId: '123456asdf', + mappings: { + alertSourceConfig: { + id: 'adnjls', + name: 'Alert Source', + key: 'alert-source', + fieldType: 'text', + }, + severityConfig: { + id: 'adnlas', + name: 'Severity', + key: 'severity', + fieldType: 'text', + }, + alertNameConfig: { + id: 'adnfls', + name: 'Alert Name', + key: 'alert-name', + fieldType: 'text', + }, + caseIdConfig: { + id: 'a6sst', + name: 'Case Id', + key: 'case-id-name', + fieldType: 'text', + }, + caseNameConfig: { + id: 'a6fst', + name: 'Case Name', + key: 'case-name', + fieldType: 'text', + }, + commentsConfig: { + id: 'a6fdf', + name: 'Comments', + key: 'comments', + fieldType: 'text', + }, + }, + }, + secrets: { + apiToken: 'swimlane-api-key', + }, + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + alertName: 'Alert Name', + severity: 'Critical', + alertSource: 'Elastic', + caseName: 'Case Name', + caseId: 'es3456789', + comments: 'This is a comment', + externalId: null, + }, + comments: [ + { + comment: 'first comment', + }, + ], + }, + }, + }; + describe('Swimlane', () => { let swimlaneSimulatorURL: string = ''; - let proxyServer: httpProxy | undefined; - let proxyHaveBeenCalled = false; - // need to wait for kibanaServer to settle ... before(async () => { swimlaneSimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.SWIMLANE) ); - - proxyServer = await getHttpProxyServer( - kibanaServer.resolveUrl('/'), - configService.get('kbnTestServer.serverArgs'), - () => { - proxyHaveBeenCalled = true; - } - ); }); - it('should return successfully when passed valid create parameters', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A swimlane action', - actionTypeId: '.swimlane', + describe('Swimlane - Action Creation', () => { + it('should return 200 when creating a swimlane action successfully', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane action', + connector_type_id: '.swimlane', + config: { + ...mockSwimlane.config, + apiUrl: swimlaneSimulatorURL, + }, + secrets: mockSwimlane.secrets, + }) + .expect(200); + + expect(createdAction).to.eql({ config: { + ...mockSwimlane.config, apiUrl: swimlaneSimulatorURL, - appId: '123445asdfasdf', - username: 'username', - }, - secrets: { - apiToken: 'swimlane-api-token', }, - }) - .expect(200); - - expect(createdAction).to.eql({ - id: createdAction.id, - isPreconfigured: false, - name: 'A swimlane action', - actionTypeId: '.swimlane', - config: { - apiUrl: swimlaneSimulatorURL, - }, - }); + connector_type_id: '.swimlane', + id: createdAction.id, + is_missing_secrets: false, + is_preconfigured: false, + name: 'A swimlane action', + }); - expect(typeof createdAction.id).to.be('string'); + expect(typeof createdAction.id).to.be('string'); - const { body: fetchedAction } = await supertest - .get(`/api/actions/action/${createdAction.id}`) - .expect(200); + const { body: fetchedAction } = await supertest + .get(`/api/actions/connector/${createdAction.id}`) + .expect(200); - expect(fetchedAction).to.eql({ - id: fetchedAction.id, - isPreconfigured: false, - name: 'A swimlane action', - actionTypeId: '.swimlane', - config: { - apiUrl: swimlaneSimulatorURL, - }, - }); - }); - - it('should return unsuccessfully when passed invalid create parameters', async () => { - await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'foo') - .send({ + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + is_preconfigured: false, + is_missing_secrets: false, name: 'A swimlane action', - actionTypeId: '.swimlane', + connector_type_id: '.swimlane', config: { + ...mockSwimlane.config, apiUrl: swimlaneSimulatorURL, }, - secrets: {}, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type secrets: [routingKey]: expected value of type [string] but got [undefined]', - }); }); - }); + }); - it('should return unsuccessfully when default swimlane url is not present in allowedHosts', async () => { - await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A swimlane action', - actionTypeId: '.swimlane', - secrets: {}, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: error configuring swimlane action: target url "https://events.swimlane.com/v2/enqueue" is not added to the Kibana config xpack.actions.allowedHosts', + it('should respond with a 400 Bad Request when creating a swimlane action with no apiUrl', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane action', + connector_type_id: '.swimlane', + config: { + appId: mockSwimlane.config.appId, + mappings: mockSwimlane.config.mappings, + }, + secrets: mockSwimlane.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]', + }); }); - }); - }); + }); - it('should create swimlane simulator action successfully', async () => { - const { body: createdSimulatedAction } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A swimlane simulator', - actionTypeId: '.swimlane', - config: { - apiUrl: swimlaneSimulatorURL, - }, - secrets: { - routingKey: 'pager-duty-routing-key', - }, - }) - .expect(200); + it('should respond with a 400 Bad Request when creating a swimlane action with no appId', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane action', + connector_type_id: '.swimlane', + config: { + mappings: mockSwimlane.config.mappings, + apiUrl: swimlaneSimulatorURL, + }, + secrets: mockSwimlane.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [appId]: expected value of type [string] but got [undefined]', + }); + }); + }); - simulatedActionId = createdSimulatedAction.id; - }); + it('should respond with a 400 Bad Request when creating a swimlane action without secrets', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane action', + connector_type_id: '.swimlane', + config: { + ...mockSwimlane.config, + apiUrl: swimlaneSimulatorURL, + }, + secrets: {}, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type secrets: [apiToken]: expected value of type [string] but got [undefined]', + }); + }); + }); - it('should handle executing with a simulated success', async () => { - const { body: result } = await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - summary: 'just a test', - }, - }) - .expect(200); - - expect(proxyHaveBeenCalled).to.equal(true); - expect(result).to.eql({ - status: 'ok', - actionId: simulatedActionId, - data: { - message: 'Event processed', - status: 'success', - }, + it('should respond with a 400 Bad Request default swimlane url is not present in allowedHosts', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane action', + connector_type_id: '.swimlane', + config: mockSwimlane.config, + secrets: mockSwimlane.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: `error validating action type config: error configuring connector action: target url "${mockSwimlane.config.apiUrl}" is not added to the Kibana config xpack.actions.allowedHosts`, + }); + }); }); }); - it('should handle a 40x swimlane error', async () => { - const { body: result } = await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - summary: 'respond-with-418', - }, - }) - .expect(200); - expect(result.status).to.equal('error'); - expect(result.message).to.match(/error posting swimlane event: unexpected status 418/); - }); + describe('Swimlane - Executor', () => { + let simulatedActionId: string; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; + before(async () => { + const { body } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane simulator', + connector_type_id: '.swimlane', + config: { + ...mockSwimlane.config, + apiUrl: swimlaneSimulatorURL, + }, + secrets: mockSwimlane.secrets, + }); + simulatedActionId = body.id; - it('should handle a 429 swimlane error', async () => { - const { body: result } = await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - summary: 'respond-with-429', - }, - }) - .expect(200); + proxyServer = await getHttpProxyServer( + kibanaServer.resolveUrl('/'), + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); + }); - expect(result.status).to.equal('error'); - expect(result.message).to.match(/error posting swimlane event: http status 429, retry later/); - expect(result.retry).to.equal(true); - }); + describe('Validation', () => { + it('should handle failing with a simulated success without action', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: {}, + }) + .then((resp: any) => { + expect(Object.keys(resp.body)).to.eql(['status', 'message', 'retry', 'connector_id']); + expect(resp.body.connector_id).to.eql(simulatedActionId); + expect(resp.body.status).to.eql('error'); + expect(resp.body.retry).to.eql(false); + // Node.js 12 oddity: + // + // The first time after the server is booted, the error message will be: + // + // undefined is not iterable (cannot read property Symbol(Symbol.iterator)) + // + // After this, the error will be: + // + // Cannot destructure property 'value' of 'undefined' as it is undefined. + // + // The error seems to come from the exact same place in the code based on the + // exact same circomstances: + // + // https://github.com/elastic/kibana/blob/b0a223ebcbac7e404e8ae6da23b2cc6a4b509ff1/packages/kbn-config-schema/src/types/literal_type.ts#L28 + // + // What triggers the error is that the `handleError` function expects its 2nd + // argument to be an object containing a `valids` property of type array. + // + // In this test the object does not contain a `valids` property, so hence the + // error. + // + // Why the error message isn't the same in all scenarios is unknown to me and + // could be a bug in V8. + expect(resp.body.message).to.match( + /^error validating action params: (undefined is not iterable \(cannot read property Symbol\(Symbol.iterator\)\)|Cannot destructure property 'value' of 'undefined' as it is undefined\.)$/ + ); + }); + }); - it('should handle a 500 swimlane error', async () => { - const { body: result } = await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - summary: 'respond-with-502', - }, - }) - .expect(200); + it('should handle failing with a simulated success without unsupported action', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'non-supported' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [createRecord]\n- [1.subAction]: expected value to equal [pushToService]', + }); + }); + }); - expect(result.status).to.equal('error'); - expect(result.message).to.match(/error posting swimlane event: http status 502/); - expect(result.retry).to.equal(true); - }); + it('should handle failing with a simulated success without subActionParams', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'pushToService' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [createRecord]\n- [1.subActionParams.incident.alertName]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without alertName', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockSwimlane.params, + subActionParams: { + incident: { + severity: 'very much so', + }, + comments: [], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [createRecord]\n- [1.subActionParams.incident.alertName]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without commentId', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockSwimlane.params, + subActionParams: { + ...mockSwimlane.params.subActionParams, + comments: [{ comment: 'comment' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [createRecord]\n- [1.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + }); + }); + }); + + it('should handle failing with a simulated success without comment message', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockSwimlane.params, + subActionParams: { + ...mockSwimlane.params.subActionParams, + comments: [{ commentId: 'success' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [createRecord]\n- [1.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + }); + }); + }); + }); + describe('Execution', () => { + it('should handle creating an incident', async () => { + const { body } = await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockSwimlane.params, + subActionParams: { + ...mockSwimlane.params.subActionParams, + comments: [], + }, + }, + }) + .expect(200); - after(() => { - if (proxyServer) { - proxyServer.close(); - } + expect(proxyHaveBeenCalled).to.equal(true); + expect(body).to.eql({ + status: 'ok', + connector_id: simulatedActionId, + data: { + id: 'wowzeronza', + title: 'ET-69', + url: `${swimlaneSimulatorURL}/record/123456asdf/wowzeronza`, + }, + }); + }); + it('should handle updating an incident', async () => { + const { body } = await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockSwimlane.params, + subActionParams: { + incident: { + ...mockSwimlane.params.subActionParams.incident, + externalId: 'wowzeronza', + }, + comments: [], + }, + }, + }) + .expect(200); + + expect(proxyHaveBeenCalled).to.equal(true); + expect(body).to.eql({ + status: 'ok', + connector_id: simulatedActionId, + data: { + id: 'wowzeronza', + title: 'ET-69', + url: `${swimlaneSimulatorURL}/record/123456asdf/wowzeronza`, + }, + }); + }); + }); + after(() => { + if (proxyServer) { + proxyServer.close(); + } + }); }); }); } From 1bab00d22aeadbfd01295952c4c65069ece07a61 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 17 May 2021 14:19:38 -0600 Subject: [PATCH 14/96] jsx --- .../swimlane/steps/swimlane_connection.tsx | 138 +++++++++--------- .../swimlane/steps/swimlane_fields.tsx | 131 +++++++++-------- 2 files changed, 132 insertions(+), 137 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx index fe3bacca8476f..a3db226708b06 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx @@ -13,7 +13,7 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; -import React, { Fragment } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; import * as i18n from '../translations'; import { StepProps } from './'; @@ -33,12 +33,8 @@ export const SwimlaneConnection: React.FunctionComponent = ({ const { apiUrl, appId } = action.config; const { apiToken } = action.secrets; const { docLinks } = useKibana().services; - - const isValid = () => { - return apiUrl && apiToken && appId; - }; - - const connectSwimlane = async () => { + const isValid = useMemo(() => apiUrl && apiToken && appId, [apiToken, apiUrl, appId]); + const connectSwimlane = useCallback(async () => { // fetch swimlane application configuration const application = await getApplication({ http, url: apiUrl, appId, apiToken }); @@ -48,42 +44,39 @@ export const SwimlaneConnection: React.FunctionComponent = ({ const allFields = application.fields; updateFields(allFields); updateCurrentStep(2); - }; - - const getEncryptedFieldNotifyLabel = (isCreate: boolean) => { - if (isCreate) { - return ( - - - - {i18n.SW_REMEMBER_VALUE_LABEL} - - - - ); - } - return ( - - - - - - ); - }; - - const connectSwimlaneButton = ( - - {i18n.SW_RETRIEVE_CONFIGURATION_LABEL} - + }, [apiToken, apiUrl, appId, http, updateCurrentStep, updateFields]); + const onChangeConfig = useCallback( + (e: React.ChangeEvent, key: 'apiUrl' | 'appId') => { + editActionConfig(key, e.target.value); + }, + [editActionConfig] + ); + const onBlurConfig = useCallback( + (key: 'apiUrl' | 'appId') => { + if (!action.config[key]) { + editActionConfig(key, ''); + } + }, + [action.config, editActionConfig] ); + const onChangeSecrets = useCallback( + (e: React.ChangeEvent) => { + editActionSecrets('apiToken', e.target.value); + }, + [editActionSecrets] + ); + const onBlurSecrets = useCallback(() => { + if (!apiToken) { + editActionSecrets('apiToken', ''); + } + }, [apiToken, editActionSecrets]); + const isInvalid = useMemo(() => errors.apiToken.length > 0 && apiToken !== undefined, [ + apiToken, + errors.apiToken.length, + ]); return ( - + <> = ({ value={apiUrl || ''} readOnly={readOnly} data-test-subj="swimlaneApiUrlInput" - onChange={(e: React.ChangeEvent) => { - editActionConfig('apiUrl', e.target.value); - }} - onBlur={() => { - if (!apiUrl) { - editActionConfig('apiUrl', ''); - } - }} + onChange={(e) => onChangeConfig(e, 'apiUrl')} + onBlur={() => onBlurConfig('apiUrl')} /> @@ -108,14 +95,8 @@ export const SwimlaneConnection: React.FunctionComponent = ({ value={appId || ''} readOnly={readOnly} data-test-subj="swimlaneAppIdInput" - onChange={(e: React.ChangeEvent) => { - editActionConfig('appId', e.target.value); - }} - onBlur={() => { - if (!appId) { - editActionConfig('appId', ''); - } - }} + onChange={(e) => onChangeConfig(e, 'appId')} + onBlur={() => onBlurConfig('appId')} /> = ({ } error={errors.apiToken} - isInvalid={errors.apiToken.length > 0 && apiToken !== undefined} + isInvalid={isInvalid} label={i18n.SW_API_TOKEN_TEXT_FIELD_LABEL} > - - {getEncryptedFieldNotifyLabel(!action.id)} + <> + {!action.id ? ( + <> + + + {i18n.SW_REMEMBER_VALUE_LABEL} + + + + ) : ( + <> + + + + + )} 0 && apiToken !== undefined} + isInvalid={isInvalid} readOnly={readOnly} value={apiToken || ''} data-test-subj="swimlaneApiTokenInput" - onChange={(e: React.ChangeEvent) => { - editActionSecrets('apiToken', e.target.value); - }} - onBlur={() => { - if (!apiToken) { - editActionSecrets('apiToken', ''); - } - }} + onChange={onChangeSecrets} + onBlur={onBlurSecrets} /> - + - {connectSwimlaneButton} - + + {i18n.SW_RETRIEVE_CONFIGURATION_LABEL} + + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index 3bc0b50a7db95..3383b7eeaecda 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -5,12 +5,23 @@ * 2.0. */ -import React, { Fragment } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { EuiButton, EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import * as i18n from '../translations'; import { StepProps } from './'; const SINGLE_SELECTION = { asPlainText: true }; +const empty = { label: '', value: '' }; +const findOption = ( + options: Array>, + searchValue: string +): EuiComboBoxOptionOption => { + return options.find((f) => searchValue === f.value) ?? empty; +}; + +const findItem = (fields: StepProps['fields'], searchValue: string | number) => { + return fields.find((f) => searchValue === f.id); +}; export const SwimlaneFields: React.FunctionComponent = ({ action, @@ -19,126 +30,114 @@ export const SwimlaneFields: React.FunctionComponent = ({ fields, }) => { const { mappings } = action.config; + const options = useMemo( + () => + fields + .filter((f) => f.fieldType === 'text' || f.fieldType === 'comments') + .map((f) => ({ label: `${f.name} (${f.key})`, value: f.id })) + .sort((a, b) => (a.label?.toLowerCase() > b.label?.toLowerCase() ? 1 : -1)), + [fields] + ); - const options = fields - .filter((f) => f.fieldType === 'text' || f.fieldType === 'comments') - .map((f) => ({ label: `${f.name} (${f.key})`, value: f.id })) - .sort((a, b) => (a.label?.toLowerCase() > b.label?.toLowerCase() ? 1 : -1)); - - const findOption = (searchValue: string) => { - return options.find((f) => searchValue === f.value); - }; - - const findItem = (searchValue: string) => { - return fields.find((f) => searchValue === f.id); - }; - - const state = { - alertSourceConfig: findOption(mappings?.alertSourceConfig?.id), - severityConfig: findOption(mappings?.severityConfig?.id), - alertNameConfig: findOption(mappings?.alertNameConfig?.id), - caseIdConfig: findOption(mappings?.caseIdConfig?.id), - caseNameConfig: findOption(mappings?.caseNameConfig?.id), - commentsConfig: findOption(mappings?.commentsConfig?.id), - }; + const state = useMemo( + () => ({ + alertSourceConfig: findOption(options, mappings?.alertSourceConfig?.id), + severityConfig: findOption(options, mappings?.severityConfig?.id), + alertNameConfig: findOption(options, mappings?.alertNameConfig?.id), + caseIdConfig: findOption(options, mappings?.caseIdConfig?.id), + caseNameConfig: findOption(options, mappings?.caseNameConfig?.id), + commentsConfig: findOption(options, mappings?.commentsConfig?.id), + }), + [options, mappings] + ); - const resetConnection = () => { + const resetConnection = useCallback(() => { updateCurrentStep(1); - }; + }, [updateCurrentStep]); - const editMappings = (key: string, option: EuiComboBoxOptionOption) => { - if (!option?.value) { - return; - } - const item = findItem(option.value); - if (!item) { - return; - } - const newProps = { - ...mappings, - [key]: { id: item.id, name: item.name, key: item.key, fieldType: item.fieldType }, - }; - editActionConfig('mappings', newProps); - }; - - const empty = { label: '', value: '' }; + const editMappings = useCallback( + (key: string, e: Array>) => { + const option = e[0]; + if (!option?.value) { + return; + } + const item = findItem(fields, option.value); + if (!item) { + return; + } + const newProps = { + ...mappings, + [key]: { id: item.id, name: item.name, key: item.key, fieldType: item.fieldType }, + }; + editActionConfig('mappings', newProps); + }, + [editActionConfig, fields, mappings] + ); return ( - + <> { - editMappings('alertSourceConfig', e[0]); - }} + onChange={(e) => editMappings('alertSourceConfig', e)} /> { - editMappings('severityConfig', e[0]); - }} + onChange={(e) => editMappings('severityConfig', e)} /> { - editMappings('alertNameConfig', e[0]); - }} + onChange={(e) => editMappings('alertNameConfig', e)} /> { - editMappings('caseIdConfig', e[0]); - }} + onChange={(e) => editMappings('caseIdConfig', e)} /> { - editMappings('caseNameConfig', e[0]); - }} + onChange={(e) => editMappings('caseNameConfig', e)} /> { - editMappings('commentsConfig', e[0]); - }} + onChange={(e) => editMappings('commentsConfig', e)} /> {i18n.SW_RETRIEVE_CONFIGURATION_RESET_LABEL} - + ); }; From b527f54f6a23bb1b1415a0b67c68112d2f7f37f4 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 17 May 2021 14:33:36 -0600 Subject: [PATCH 15/96] schema fix --- .../actions/server/builtin_action_types/swimlane/schema.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts index 159b4339b74a6..9f775003dc666 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -18,11 +18,11 @@ export const ConfigMapSchema = schema.object(ConfigMap); export const ConfigMapping = { alertNameConfig: ConfigMapSchema, - alertSourceConfig: ConfigMapSchema, - caseIdConfig: ConfigMapSchema, + alertSourceConfig: schema.nullable(ConfigMapSchema), + caseIdConfig: schema.nullable(ConfigMapSchema), caseNameConfig: schema.nullable(ConfigMapSchema), commentsConfig: schema.nullable(ConfigMapSchema), - severityConfig: ConfigMapSchema, + severityConfig: schema.nullable(ConfigMapSchema), }; export const ConfigMappingSchema = schema.object(ConfigMapping); From 617328fa7c832eb534991c7632e3848fed69f67a Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 18 May 2021 07:37:29 -0600 Subject: [PATCH 16/96] fix type in test --- .../server/builtin_action_types/swimlane/helpers.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts index 9cad8461528dc..46e950de8679b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts @@ -64,13 +64,13 @@ describe('Create Record Mapping', () => { externalId: null, }; const data = getBodyForEventAction(appId, mappingConfig, params); - expect(data?.values?.[mappingConfig.alertSourceConfig.id]).toEqual(params.alertSource); + expect(data?.values?.[mappingConfig.alertSourceConfig?.id ?? 0]).toEqual(params.alertSource); expect(data?.values?.[mappingConfig.alertNameConfig.id]).toEqual(params.alertName); // @ts-ignore expect(data?.values?.[mappingConfig.caseNameConfig.id]).toEqual(params.caseName); - expect(data?.values?.[mappingConfig.caseIdConfig.id]).toEqual(params.caseId); + expect(data?.values?.[mappingConfig.caseIdConfig?.id ?? 0]).toEqual(params.caseId); // @ts-ignore expect(data?.values?.[mappingConfig.commentsConfig.id]).toEqual(params.comments); - expect(data?.values?.[mappingConfig.severityConfig.id]).toEqual(params.severity); + expect(data?.values?.[mappingConfig?.severityConfig?.id ?? 0]).toEqual(params.severity); }); }); From 2f23cb45fc335f56fae0879e80e191c768944003 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 19 May 2021 14:03:26 +0300 Subject: [PATCH 17/96] Minor improvements --- .../server/builtin_action_types/jira/types.ts | 6 +-- .../builtin_action_types/swimlane/helpers.ts | 49 ++++++++----------- .../builtin_action_types/swimlane/index.ts | 1 + .../builtin_action_types/swimlane/schema.ts | 5 +- .../builtin_action_types/swimlane/service.ts | 22 ++++++++- .../builtin_action_types/swimlane/types.ts | 5 -- .../cases/server/client/configure/utils.ts | 3 +- 7 files changed, 49 insertions(+), 42 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index 1296782309d73..74d53901d55d9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -124,7 +124,7 @@ export type ExecutorSubActionGetCapabilitiesParams = TypeOf< typeof ExecutorSubActionGetCapabilitiesParamsSchema >; -export type ExecutorSubActionGetIssueTypesParams = TypeOf< +export type ExecutorSubActionCommonFieldsParams = TypeOf< typeof ExecutorSubActionCommonFieldsParamsSchema >; @@ -157,12 +157,12 @@ export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs { export interface GetIssueTypesHandlerArgs { externalService: ExternalService; - params: ExecutorSubActionGetIssueTypesParams; + params: ExecutorSubActionCommonFieldsParams; } export interface GetCommonFieldsHandlerArgs { externalService: ExternalService; - params: ExecutorSubActionGetIssueTypesParams; + params: ExecutorSubActionCommonFieldsParams; } export interface GetFieldsByIssueTypeHandlerArgs { diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts index 6d761e8d8a277..daa34bb731e6b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts @@ -28,11 +28,7 @@ export const getBodyForEventAction = ( const values: SwimlaneDataValues = {}; const comments: SwimlaneDataComments = {}; - for (const mappingsKey in mappingConfig) { - if (!Object.hasOwnProperty.call(mappingConfig, mappingsKey)) { - continue; - } - + for (const mappingsKey of Object.keys(mappingConfig)) { const fieldMap = mappingConfig[mappingsKey]; if (!fieldMap) { @@ -45,29 +41,26 @@ export const getBodyForEventAction = ( 'Config', '' ) as keyof ExecutorSubActionCreateRecordParams; - if (params[paramName]) { - const value = params[paramName]; - if (value) { - switch (fieldType) { - case 'comments': { - if (comments[id] != null) { - comments[id] = [ - ...comments[id], - { fieldId: id, message: value, createdDate, isRichText: true }, - ]; - } else { - comments[id] = [{ fieldId: id, message: value, createdDate, isRichText: true }]; - } - break; - } - case 'numeric': { - values[id] = +value; - break; - } - default: { - values[id] = value; - break; - } + + const value = params[paramName]; + + if (value) { + switch (fieldType) { + case 'comments': { + comments[id] = [ + ...(comments[id] != null ? comments[id] : []), + { fieldId: id, message: value, createdDate, isRichText: true }, + ]; + break; + } + case 'numeric': { + const number = Number(value); + values[id] = isNaN(number) ? 0 : number; + break; + } + default: { + values[id] = value; + break; } } } diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts index 2730b6cdf3baf..5abb7b2e5c5d6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts @@ -112,6 +112,7 @@ async function executor( logger.debug(`Swimlane new record id ${data.id}`); } + if (subAction === 'pushToService') { const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts index 9f775003dc666..86c832832ae5f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -34,13 +34,13 @@ export const SwimlaneServiceConfiguration = { }; export const SwimlaneServiceConfigurationSchema = schema.object(SwimlaneServiceConfiguration); -// secrets definition export const SwimlaneSecretsConfiguration = { apiToken: schema.string(), }; export const SwimlaneSecretsConfigurationSchema = schema.object(SwimlaneSecretsConfiguration); + const SwimlaneFields = { alertName: schema.string(), alertSource: schema.nullable(schema.string()), @@ -49,6 +49,7 @@ const SwimlaneFields = { comments: schema.nullable(schema.string()), severity: schema.nullable(schema.string()), }; + export const ExecutorSubActionCreateRecordParamsSchema = schema.object(SwimlaneFields); export const ExecutorSubActionPushParamsSchema = schema.object({ @@ -66,8 +67,6 @@ export const ExecutorSubActionPushParamsSchema = schema.object({ ), }); -export const ExecutorSubActionGetApplicationParamsSchema = schema.object({ id: schema.string() }); - export const ExecutorParamsSchema = schema.oneOf([ schema.object({ subAction: schema.literal('createRecord'), diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index 15293456bd624..1f4bdd440f258 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -43,29 +43,35 @@ export const createExternalService = ( const headers: Record = { 'Content-Type': 'application/json', 'Private-Token': `${secrets.apiToken}`, - 'kbn-xsrf': 'why', + 'kbn-xsrf': 'true', }; const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url; const apiUrl = urlWithoutTrailingSlash.endsWith('api') ? urlWithoutTrailingSlash : urlWithoutTrailingSlash + '/api'; + const getPostRecordUrl = (id: string) => `${apiUrl}/app/${id}/record`; + const getPostRecordIdUrl = (id: string, recordId: string) => `${getPostRecordUrl(id)}/${recordId}`; + const getRecordIdUrl = (id: string, recordId: string) => `${urlWithoutTrailingSlash}/record/${id}/${recordId}`; + const getPostCommentUrl = (id: string, recordId: string, commentFieldId: string) => `${getPostRecordIdUrl(id, recordId)}/${commentFieldId}/comment`; const getCommentFieldId = (fieldMappings: MappingConfigType): string | null => fieldMappings.commentsConfig?.id || null; + const createRecord = async ( params: CreateRecordParams ): Promise => { try { const mappingConfig = mappings as MappingConfigType; const data = getBodyForEventAction(appId, mappingConfig, params.incident); + const res = await request({ axios: axiosInstance, configurationUtilities, @@ -92,6 +98,7 @@ export const createExternalService = ( try { const mappingConfig = mappings as MappingConfigType; const data = getBodyForEventAction(appId, mappingConfig, params.incident, params.incidentId); + const res = await request({ axios: axiosInstance, configurationUtilities, @@ -101,9 +108,11 @@ export const createExternalService = ( method: 'patch', url: getPostRecordIdUrl(appId, params.incidentId), }); + const fieldId = mappingConfig.commentsConfig?.id; let potentialNewDescription: SwimlaneComment[] = []; let isDescriptionPosted = true; + if ( fieldId != null && res.data.comments != null && @@ -129,6 +138,7 @@ export const createExternalService = ( isDescriptionPosted = existingComments.some( ({ message }) => removeCommentFieldUpdatedInformation(`${message}`) === messageString ); + if (!isDescriptionPosted) { // if description has updated // post as comments @@ -139,6 +149,7 @@ export const createExternalService = ( }); } } + return { id: res.data.id, title: res.data.name, @@ -158,15 +169,18 @@ export const createExternalService = ( try { const mappingConfig = mappings as MappingConfigType; const fieldId = getCommentFieldId(mappingConfig); + if (fieldId == null) { throw new Error(`No comment field mapped in ${i18n.NAME} connector`); } + const data = { createdDate, fieldId, isRichText: true, message: comment.comment, }; + await request({ axios: axiosInstance, configurationUtilities, @@ -176,7 +190,11 @@ export const createExternalService = ( method: 'post', url: getPostCommentUrl(appId, incidentId, fieldId), }); - return { pushedDate: createdDate }; + + // TODO: Check if commentId and externalCommentId is needed. + return { + pushedDate: createdDate, + }; } catch (error) { throw new Error( getErrorMessage( diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts index 3ee192ead4443..92cd121dc4717 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts @@ -12,7 +12,6 @@ import { ConfigMappingSchema, ExecutorParamsSchema, ExecutorSubActionCreateRecordParamsSchema, - ExecutorSubActionGetApplicationParamsSchema, ExecutorSubActionPushParamsSchema, SwimlaneSecretsConfigurationSchema, SwimlaneServiceConfigurationSchema, @@ -87,10 +86,6 @@ export interface ExternalService { export type Incident = ExecutorSubActionPushParams['incident']; export type CreateRecordApiParams = ExecutorSubActionCreateRecordParams; -export type ExecutorSubActionGetApplicationParams = TypeOf< - typeof ExecutorSubActionGetApplicationParamsSchema ->; - export interface ExternalServiceApiHandlerArgs { externalService: ExternalService; } diff --git a/x-pack/plugins/cases/server/client/configure/utils.ts b/x-pack/plugins/cases/server/client/configure/utils.ts index 2454d9f739fcc..9f1fa7f936c94 100644 --- a/x-pack/plugins/cases/server/client/configure/utils.ts +++ b/x-pack/plugins/cases/server/client/configure/utils.ts @@ -13,7 +13,7 @@ import { SwimlanePublicConfigurationType, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../actions/server/types'; -export { SwimlanePublicConfigurationType }; + const normalizeJiraFields = (jiraFields: JiraGetFieldsResponse): ConnectorField[] => Object.keys(jiraFields).reduce( (acc, data) => @@ -112,6 +112,7 @@ const getPreferredFields = (theType: string, swimlaneMappings?: SwimlaneMappings comments = 'work_notes'; } else if (theType === ConnectorTypes.swimlane) { title = 'alertName'; + // TODO: Where we should map the description? description = 'comments'; comments = 'comments'; } From b3e2ff6ec0588539131744309dde27090fb5b772 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 19 May 2021 17:05:09 -0400 Subject: [PATCH 18/96] Adding swimlane docs --- docs/management/action-types.asciidoc | 4 + .../connectors/action-types/swimlane.asciidoc | 102 ++++++++++++++++++ .../connectors/images/swimlane-connector.png | Bin 0 -> 74730 bytes .../images/swimlane-params-test.png | Bin 0 -> 168149 bytes docs/management/connectors/index.asciidoc | 1 + .../server/builtin_action_types/jira/index.ts | 4 +- 6 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 docs/management/connectors/action-types/swimlane.asciidoc create mode 100644 docs/management/connectors/images/swimlane-connector.png create mode 100644 docs/management/connectors/images/swimlane-params-test.png diff --git a/docs/management/action-types.asciidoc b/docs/management/action-types.asciidoc index ec5677bd04a6e..29f1db1cfec5d 100644 --- a/docs/management/action-types.asciidoc +++ b/docs/management/action-types.asciidoc @@ -43,6 +43,10 @@ a| <> | Send a message to a Slack channel or user. +a| <> + +| Create an incident in Swimlane. + a| <> | Send a request to a web service. diff --git a/docs/management/connectors/action-types/swimlane.asciidoc b/docs/management/connectors/action-types/swimlane.asciidoc new file mode 100644 index 0000000000000..1f29d749664f7 --- /dev/null +++ b/docs/management/connectors/action-types/swimlane.asciidoc @@ -0,0 +1,102 @@ +[role="xpack"] +[[swimlane-action-type]] +=== Swimlane connector and action +++++ +Swimlane +++++ + +The Swimlane connector uses the https://swimlane.com/knowledge-center/docs/developer-guide/rest-api/[REST API] to create Swimlane records. + +[float] +[[swimlane-connector-configuration]] +==== Connector configuration + +Swimlane connectors have the following configuration properties. + +Name:: The name of the connector. The name is used to identify a connector in the **Stack Management** UI connector listing, and in the connector list when configuring an action. +URL:: Swimlane instance URL. +Application ID:: Swimlane application ID. +API token:: Swimlane API authentication token for HTTP Basic authentication. + +[float] +[[Preconfigured-swimlane-configuration]] +==== Preconfigured connector type + +[source,text] +-- + my-swimlane: + name: preconfigured-swimlane-connector-type + actionTypeId: .swimlane + config: + apiUrl: https://elastic.swimlaneurl.us + appId: app-id + mappings: + alertNameConfig: + fieldType: text + id: a6fst + key: alert-name + name: Alert Name + alertSourceConfig: + fieldType: text + id: adnls + key: alert-source + name: Alert Source + caseIdConfig: + fieldType: text + id: ae1mi + key: case-id + name: Case ID + caseNameConfig: + fieldType: text + id: anxnr + key: case-name + name: Case Name + commentsConfig: + fieldType: comments + id: au18d + key: comments + name: Comments + severityConfig: + fieldType: text + id: a71ik + key: severity + name: severity + secrets: + apiToken: tokenkeystorevalue +-- + +Config defines information for the connector type. + +`apiUrl`:: An address that corresponds to *URL*. +`appId`:: A key that corresponds to *Application ID*. + +Secrets defines sensitive information for the connector type. + +`apiToken`:: A string that corresponds to *API Token*. Should be stored in the <>. + +[float] +[[define-swimlane-ui]] +==== Define connector in Stack Management + +Define Swimlane connector properties. + +[role="screenshot"] +image::management/connectors/images/swimlane-connector.png[Swimlane connector] + +Test Swimlane action parameters. + +[role="screenshot"] +image::management/connectors/images/swimlane-params-test.png[Swimlane params test] + +[float] +[[swimlane-action-configuration]] +==== Action configuration + +Swimlane actions have the following configuration properties. + +Alert Name:: The alert name of the incident. +Alert Source:: The alert source of the incident. +Case ID:: The Case ID of the incident. +Case Name:: The Case name of the incident. +Comments:: The comments of the incident. +Severity:: The severity of the incident. diff --git a/docs/management/connectors/images/swimlane-connector.png b/docs/management/connectors/images/swimlane-connector.png new file mode 100644 index 0000000000000000000000000000000000000000..520c35d00381bd8d21a0accff1ba56cc1145ffc4 GIT binary patch literal 74730 zcmd432Q-{r)HXay1kuuns3C}G2|<)Wg6Kr=En4(W7&Vee)M(L3qD7R^+ZZJxI#Gui zErQV}+Kf^EJI`AlyzBqh`q%ot^?lYFZs$IA?^E`^_I2jnQ*{MO@|)x!5QtJqQBDg4 zB5DCX*GNf#6i+|(&ma)N6MI?Nr%JN2tWP~$ZS9?GK%hHuRu&dllz8uST3T9IboTLZ zlY97Ty?q;{Wf9!n(#_i2($&&ok(Od&GJAt&mPqFtC`Yrk){!ELb-#kYvp5;nKU$Ef z92 z%8vI=PAYkchz|Hk-ZaafoSbAGoSe*XP*C)W(%+!EMhL>}nKO3fxQPOS)w3~BvQ<+9 z-36XWK?LFUAY$N&0QlSlK7jn=-+(Rxf3E-^xsQbZ^_HmRBhi09U;CT!k+!Uo67W~s z%EQLS#q+tVSH)WcdZ4N?dmRHW12t7~D_3WJOKaC>HvBJ~-ToELpghAKs-PjQlbwZO8%4of4up(#Zx5>{w*mca_@A>Q*X|f z)b+ISkacwin)H(TS2O=v`1H&FEGQ}PcjTue@gHLTCl!#h6uG3pf9p(&{D?HF2?UY> zDak$3c|ou?bs(I;m`A;VhXi}R;ar%|Q;vzz!Z{A}s>v&0!8 zq7Lmu>}+f%0vBWa;{#92@vm5~G=yBG*(RKQb$q)}`iwN+cLIe)YWix5WMe2WH1W!S)_OFOYi^yI z0l>%=Kl(-1R9GJUcn0Q6YC_MV=Tu{?;;`2H6A899(n-Y3A#>eDqwxL6ja7lLFzx;}|^JSPSh zn9Rs5qP}&V{N)jLZQ^Pik3N5aiPV|KfK^n-L)d3rRgJ+%Z|ExAWc9`W2^MsipH2Uo zJ+mzS3ZZ#l$yfW zw&uGMXjYlg-c)2{appnvtTQJ+_0_W^{rZl!=lj`bf>6fY$pXH8^(gm`kK4aixXwr! z>-1-c*(}FjsLdI7?B4rw(6KeFAq|VcOqR>XbE>T%?btU-O&YwEs92=t9Nd0H(cgaf zaJ+he`-qu_S651hvy+xBM!Dy-lwa-NiW z6zLZ&B5jF6B{<3g@XCcc^=YfFi%}E4D|(5M|1R_vCOQjJFN$~Jlnl45I-<8bOWmztl}uzoE&xE%#Kb{tQ3nTF>k-hMPC}1>idos%erFi-W%g9TBAokPU7& zWZ2?u$`79Nof~W2pXDPLwiY^>_bBLxP>20p-m4;=%RG^$92!}w-5=okBCgYH_F4+x z4i7LShzu)uGJi}Sx8nz1t&bK#gMQuHivsV53)EGT?k0gZAp1M?hGlM_GK3L2MP|Ac zHhn9TB0sV(QZpy^rPp*G6J5ORn>DGG?`5x)baz1*0X08?W{7)tGyAUOoAo3MBrw@h zc^@C*bG7m!5J%-Q=-W`^W;69nNsftV^VeHAr3&k=(t1QR)sp8#nPtw}L?zZ1Ut?$pYDlcQkw1 zV>gy?k5KEm5DXvRqeqVfeK!cjUi=A>+_R8XO6Je>Ogp{eT+(G4$wH(v0}b_xqbW#m zneXlqxE&gK*{{}=Ffa9`3A?Xfyl|m2oW7)A`K~ z7MKgoipj^wAoxiTUFt*oc+bw=Lg%GzH35B#Qm*N>KQ%@DMzvpUAC>T%N&$;|AX~bb zg8r~Tj*?fnEO=p5RP|$Hn>Y2;Px0pC{i30!qgCdP9F%5945mpyJ6{#fsWQx|Hoppm zD0^t}Z4GI(W^l=l@63_K39=fZ>L0XO=p26haZ?ux?c2ndu6u}VaBImmc(2|S zcGUBw>Ke-HS?DUra$l#u@h7rzzk9V5Blqd?>)dDUTcz7hr>qD!81aTbC}1UCG5m7! z!btfZn6AER`IkxUhJ)+4&_JUF;gIM;lZVZEWv7+@qs-Z{qhE=)Y*z^Mx;&uH#509n!MeQu1|+^t#?&W-skJZjP0{j~idPeCWW*)G)Pt z)Gq>ZnrZ-6K|RYu2)6iv>)?J`$VgX?^Yig9X=_iI!Rq@O`yqQ zU*E&)Kv{MDgKZifd^B9(Ittei^HKvQWiVW0bK7S9aO;en9={of^W`#V+ z^y2C5dzOlHa#X&Ib}jNf9m%VXgIWEl)zOmQt@2Cjx5O%=3 zbkycOdre*{iA-2AQ$UbaehS@amAxn*-20RH=a=PJ>5KAUYaFIZ!->z#56|JZ^eJ3& z1^LBNc015jz*|kAE`R3a#{A{OmBtrfDEw$c3G%{Q?p1BBUfPb)E;)I_YIBcY5ogE0 zMnZs0%ZW?%uUg&Yh1IxZ4&Hf}QC1&ZiFQFvz>1x9bCulCJgQ}0)BNHc%t@s} zt7aI&rblmlTP4|_q~8B2n%L2(=7C_Wi{l^td{z6YDEFI(h-{zEow@dHX#Eq2okG+N zhm8e4b@a>WI}80uJtUuEYaO`;qzG%A=V3qPBNzRNG0kb8ItKHYBz*lTM)~z? zidEAdj!~Fz+`UBGizR*85zXWpIpIR+j{7wwJ`ynhHrNRkPJLp$0mqpk)))KJxr}RF zeAi*FRnMnQ#){4C^@y|Y&IHeN93^Jbt+}^2qt)?~QSB-MU;z!O!Tu5-w!SoxxEM?$ zhxh6ek{0v8-RchfyQ@FOw`T$msvCFaB1=8@b6S1|H=cCH-?w@~FX59ISd#3&9N>H9 z)M@Ygn6)2f;~E!l zrr3sZcUfzhb;jW3cDl7^Dh*KF$;N@}8u@esrV-^c+?<6gHE+%$CMJGPdtf+lW9gnJ zSBV+ii2VB9*|i3#?0ca({gjB2y-yd_{!Dl628wOYkm*a|q9v9+g=jatw`&6DzLUkG z%}R`P2Qe85H^JjF`zL!jm5xC)23%XYj)>~b9}Lj?{3~@-6``k1Aey(spchkdq|0D1 z+an6Un0_N7#iP}UZBHR;uWHro``T~P^>2%{J=_&%N<#srqE>~qI|H$<$fG5S1dU_#?2Ylc+9jb=% zFrxc2QNQaO)Ek^9m9oFAN4$G&Jlg2;Wc?_{LAxOl-T4|kXt2M z2$HlnenpXy<@QDrChIt`v7+l}60V%mq6cXJtXsPiaX2m}V7vrYN8OPr?D&B)WS7nK zdpB7~GOy`faq3Ye9HNx_)~#EgBk$X6W75uO8B5=78-Fh{z z+2=ZX^2WX#(LZ`C^wk}xz$TXw4EblTJeG$z^g)L`0?lFtYoBE_%_--_{pdtU4n=-?>r*Q4?mp_52r#Y*_%57*Oj_m? z-CFEo--hzO&6AH}5FN65?yMTwXEhC5%c|-ygDhm`dTQDAra1cwKOFWe+E=&2Rk;na z6`I06tiIoX6+C`T-hlbhh3(q|HkRm{IiBC9hF{{dJe!l#K5cFky2X9lfH|G=^Jvh7 zS*8ztm$3Z@fmT`p8v#LDY}(cXQx#V`7Vxiya);CMJUDqDa3;^hNnRj-L6x~aj5ZeP z#Zw3A!ktA-+v~S4n%W%hZDL(JVwkWm^6toHbfmSHSFNY*Xyc}| z$+r6!>eNMl18W<}9~?Yjw&RSdtxroA^W=5#ceDbkPBU9Y-*skD3fCt>eDE zhORRe85ZpeBPL6TZhnC^H!KZEpTtA`cY)OhOVwX^04o`F7%NWgfJ@p!cCYql{BQB8sMuPcsm{kJ-NwC977wpxbTWJ`mil*x+KB1kh2qJC zri(_|y3uLWFN*U^+K$sj?ANlAoKHF6b0)K;O&7=Qoh`1!p>N_<4!VzP3UE2QMb^_z zoy=lf1NirmLyl8|^u9|Dpt+35$N`)LoM)Zcks-%85LHln?H;DVTVwj=#KF|`Wk&Cm zDl_8}2VET7#y(coR@jPbxp4{FeEcNn!1-h5!D>l3s*3@oVG72hvk&|lHxz>{_8k9M zAXWy9@|(t%>d{&CE}Pi_$Tq9~YV0$pLH*G0n|x^b%Ewg+JKp*m+f~&iLbkm-DZh&` zMS+;9n0^lTeZNX6QL8v{@4K-$7Dw6Lj_}v|W)q|NYO9XZL?r{=%bJM8eW`gf^T5kR zqrJQ0rw`X!_CWCu%5mXG7fmyQ4(4=!@@TltZ2gewoUQT&8OE zb^`-)Gv%u#wR%OvWsitbMV#ZS97gBTkjp0KxZFsEWuEv9ui%oK{rN{DxITwJzp@A? zX_?3=8pQ)88h-mbKec!dxy+0rIF5rZ*K11I5#+f{iT42B2X@bX_Q0jXR~0&w!)yaV zZ8mR--$I*&_+5{Kc$kUic6jBj180Gv=P(1Ku)WOaCs_!+_>MrVZfVZq@X>=pZ4_?L z0X{kjUOqN$#Pp>&5=C=L|Cnhp>sVWe3mk(5pMZNRn7gSa!JVp2VS!q+zj`o*Bx z8-89VJ_nOgug9!r0rO}Y#o4f|4D(s9Kq319pX{$IlbTt%(QP3zQmgd)hoI{Quh3zA z;;N2LddtV685@00k$!9wjXv&i0fd|kkpWF^y9G5xej z(^GY`hrflP2d!?~{m3=?%1%0*o`*DJ0ZP2X{p6VFGp4c6#SEN_8CB!ue(=BqyEe_I z6wet~XOO#kh~NIP|8WTxQEA=fRFXrYdA}TiNA6EQ%j`*Sr;tEYxFqg~u}Hp9jA4;> zL4B2N{cYSp54Z|Hb+f2b&>x`#5gP}ko<-D5;?Mj8hf_ixDg@+wmiiY)dVTk`sXQhob-kIx2V1((Kv*_v*a~n3FU8(1yqJ+ z8{>K93CJjvxvmyq61alKE#63nE8)H9=U2&vZ|hoUk%{56Gf*(&$G!J{SJiJRV*M#e zgIvF5s+`i?Uwo^#LBjntjlWAhBeJ2Jq)90O9Ty@qa3Kr3F3G16;5^OVfXN^W3Y&*@ zSKpR(n`e_1>()=)S>XAut6NWf%3uYA5JFANUzEHtlgj-!zDkvSaB+kNDJ9{r_BCUhSnI_w%=;IOJ05p_bqZG@E4BT}83HZ4C1 z-Z>;}#ZZoRo`pAAX|9GoBnzolz@hac-jUP|=7qC#^T3g`iB(-hdFJCBhBWS){3>!@ z6JqDk^$?k2;yG*5sxd_(V@kIJJB?c>Q8No)t)EuNEaSEAORV9&}tISnN)@cgNe@hu7YDQHno3*^_ox69+s`?950(i4K zGstnlTXCn3-v*21mt93ZYudsbvUUmv_&ZV){n=9D7DUM)r8?(*R~LV& zdj=l+_$gMdg8zZe-hKa(aJKi#9j2G@<|Sa`bJLUb`%_kRz0ep-)6aeRvROs@9FcUb znBWeL?w9gCHap|o6)(E()(50sfPD^hyD#ZJH_x3a;i^>if&GbEyrCy;d)WibL=2O} zEg@UJ_nTMkI!mVx8to!l2UU{!l}@rRcGaxIp1nUn@_W;ct~_L3YeagsRNwPkF1=vDcVc5|Af&=jb|e%~+_=A`t;HIgC85`Rbdjp3 z?APywJUS$RwKa+2{7^Nfg0r|!TIJv1@el0d9U1NV%vcR8Z9g11>NPEzs8XqBN-4ZK z1plyA`@owTbwlitR=(=3DOeNVvQTcc(&bXg0`<%|4i!D+G2YZ%i_pf>-}k3dnk`a! z4I7lZllbU|RXWc^LTZ&*&8S1EK7j05QcScu!thNh$t5+duGL?!=bEAx<8LcnnQ$Pi zW!a}poDK~7;0nRfP)1LBln616j4LuFG2OrqZm> z1)OjvyB{9OKIvkBf&+6@ zZ;!$hkibz@S9=X!P}caz#26NwBWQYEm_gJSdUQZNA>$4uyE&Y%#!k618tmZbvPUHeJ965O&OPEbD=q^|+pIWE)Y-%4JLB4^+)G_c1HTLsEry3>3R*AS5cA)u zyT)UrxV6x?aCZRpUTDJYVdGJP`Z~%mpTNw-(MjBp z%>P<3*#(?uvwcxbz?**AX-j&rG%1nSJ=S})fk`~T;CQNTv`Vd74mSac3``Sn;q{)xyY+Ja44)k_3OEG&_ z0#~IUze(Ml;A7#ThpzW&FaP-RY2>SBSJrQq(^~y=txm}FLl*AsXjVV21f4BucA@#E zb%s>nlba(Y-fG4X*ZKvBg78R^W`C`49*uk3sT$cG3J7)!vV*xkSz6}zieF(4GJHeDfYuv! z6s_J+h+*En<2g5P=+o)-BWige_EDR4+Z#TF+4GqBp7dcXGqvCkV3K++aQkcAlAtgo zn*8|jwqWotEreKRYybf9Ue$TBXx?mD$tZ!%o)@;UA6gf(>z{BgL5*)}&(9YQYYh_@ zNT{M_F#D;hnIec9G2O3;TiXJ`sfTvXLAc-;Mlnv7U}VSSs_lK9-X#xFtVE?V1ths1tJ7Ws|N=xC*vO{Y-i6Z zkmju@xTQ;NmciWIJ(r*;9|!`xn3bg~u zaccL3dUkfCo`s<+@>CSxU6EhI>u_r$#mdD4MAK6b{9Tvl1g(BZ7HZXJriykB_O)We<(OQN})Xo!%thaFD#`~=~`)S*4Nz>A%9=Gba@@j`dfhbN02?-=#7>|Sr>Ia3$$Kll0bQ?vg0 zn=}9eHqt9avgRq%3TLmbHT56tirAT%&IIFxA;!T%`li9XZvMxPh_7txGb^?)k(8V= zrAubQN}1wGl4CV_I)d)pP&%K*!IcBAZyfwjB}|jjrrLBT)pk=$vv!6 zZ(D-3sHqAU?(_tr#APUoT|R67hAFg)V+cz+$bd+l$zd(`@rCVoLL(Dmi^qkR5_gYR z*d=!S-_ciqgO9Hm*14rho=jTRD8LT`ZLFh~b*C!4!!r>jt*#|$y1g`uuk%y%DtxA% z6k5V3>@UK&-&-5zoM8fCn&Gpf8NNd1BAYGy8cBW*-Bd@13yg*f~5Z`cT`$9?Q< zbN>>HJ<^hpifIeLF%+JL6Qb{LD|;^X+G^nIZAhu9Yax%62*F`e!)?j!I~xxBb45)R ze%n4>HvXGlHkRvs!}XquZG_b8^ay{`Ri0{+yHRsm7Af2mD5_gTf#cBxV* zc(f~FO&L>A8Jsf^0vfp9Iu*9OYFZRpaa=77FUS-RcA*t!a3!)>CmZuAegKAwV``$7 zqXm3jmFU5?74#(V(VBvnWbrvF{jp4gnXwyti+mDmhwi4S4KFsoVYBG^%Fti9?=3k% zs>3IgE-zA=AboIHJ3-lRy8;izFg-pQr_HvO%d8-u+ojuuZQ7C7xGfg_)su~E6&*dU zmOWIO;gRqdkz4+G?57V}%NW+^OYh^2Pn2>t^4rAwlxmC9>Cr`tyS}M%_=C!I2is3q zg@UCWv7b8wG-f1NKvl4dc^=I9J;Cv8}3N zz8w6Vu3i;UdP5P{mIrmn?S?hZ_m|J6`dBUbp)%05=CzD^I4th-{0Q9f2bA+_=9>Ox! zMo;({3p1CWd0kZatR1o*Fp^qHm!|tZw)@MV_WaLJZMH${M}yOF_4O5>n3ATKicrm- zK9@*sAmpT$^^+N5`tz%Awz*me>`4_umo9CnWz($IujcyQM4;Cs9rBU1RlYpBTgd-Stlh{#DQS{|eEAm1j8#82K= z)niz*VC*Qhk7cn+@-q>WnUaShr>yipegys@R(o%~Uw9(=UAr;CRFy?k63M#vp;8Vw zgUD49MKv`{X(?kd&)!OQKiMdbY&BvaE$cp1Z@-Ut_~tc7kY1@1H;Q4I*tB6!i=*;N zRlkyHvt>d6-jA+cGgbYl6z7*CScRzUYg2QUDaDQX>qGShM}MPwt5m`zPi)MFb1x~{ zPo+!h%*+?ktUF_P^t%`7rQOp^$5q0aSLcip3VN?d2jdV^`#u>#%e(erOftQ#+MfasZ2JFuAP(i)REJL-TsoZLnA5%>KpcnPq2ia7?U?;Q4 zoJ&vaS(o{iziHo;T<-p%-e8uhp2ZM2{Uxfb%nf!gq1NAVLbPETbyUqKu0Os!h5dl` zVHws1_C*O_)8DATft198OV-QVhzpf2BFn4sUIq|1^2Oun;#;SfJ$p?cf-;P>mC@UO zfyTd27Q~Id!jDg(kN><5i6x@}9gGv@iJkU@0Jm=X0%-^w=63D$kZELEI*3U_0@p%^ z7*CssfRBy=2r<*94;!FLg!{p-%zdH_b;?OF3X!L%$6RBDgTRtA&;s6_ww3<1!JFI^1VUyu1;2YN3B zNR9cgEIA0_v)2F36uk|!Q>rk*J^1Rk{0rm1+x_DmeYCtj~ z>(2Ult=|RIoAf&DoYtHEYTZ#I5qn*jy8Ud^QpIj?ncKeD{f{t^Y!3Ncb$-7n0_6L@K)i!0qh{X zH3vq8dW~!;zUcI-_XN$9Q8^MG3}Xv2iFgTp{=oq0lfxpX@jp=Oz7M+MUe5(mg={L|-*4VQd@)l4sDFHs zw0!a1NjyD51CrdKp`k-CUzU2eUoR(p&Bf@29ipte(hR3x9C<5cOJ%|b#+*^}u`?%$ zUKx5mWkPVV$}vUyO#?y9IFz!jYD;>vy>fZytdVc@P|J6#C{X(pG4N>Jf7 z%wZR);{fK>yX@@j#>1^ht*<&to@UnHUMUIMZ&?F2W+4TWh^pC(V=-eEZ~?pvQPjLR zD591ru7=%^V9irWibrQbi~t7ohJ$foJcrHv(tWK)15Jpzw^6-E(qHbF{pi=cy@LbT zifC{vp;G+qca2yikB3Gan~~&MbXswT(ESR?b+S$g=rK<$82cqxEq(0uG%noLW~S*V zp3~cb44_Dc-xN(=N7bsdlV7)vAR|$TVdSvth>1VMZ%xSfA*<};1#RwK7cjLC1(*j$sh@uNo3_5hFKi3=cb87V zauNDqQ|BIlN=nQ{{Q=NP=lkYxTu-`lT7zi=ouwzH!aRH`#QHbVSfp zT}0F)$-mm(jC+=Lmz^RW07unrt*%tC2o^O86^GU z1bmhEm|dGwqwLZ4tIeyXh+K{2M<4hN??lq}SHdd+2*e|EIJ!`qt82$TAva=~DND$% zbp1N>J&TWh5;j|M%QF}VelK{+X8Q1g$KD@H1Xu!&7@YR}{wFxsCG(>7>R1Vq zIK-}M2>OrQ!<)x$d|pr&B4~o7i8xn~1$CfgyW%;L4J-4zK1v7IIvZD5r#cUOd=!?a zmL9G~a~N)qB4*f$We{zJ->5mW8;8pA{Eg6)c%_Y%n4<}pn3-K5o&x4gya3u%hW@Hs z^emh%`Htq~4mSV2m?yfvv{0w;Ge8(gY2kl;jn7C&J>%go@#OnfBwYy-weT9%R4wct zFp+GA^ZDWy^q?Gm+t(|$V|H8OIaEw>#b)rY@KWopE4G%U=D9xW2YpTO-rWI!;JaS4 zIBJP#ZEe-6*Wxg&ajK`R%8U|r94oUCJJ`q`9F@Lv=c#RqMN=;vr_`l|>n^wISOEB$ zr7G3V;@3=q?>q&-A956AUa;+lu;~NvFA3ibAh0G|vd*hhmy!X7^b>b`uDKtdJ#enT zWHN|bZuUA1O)F*=t>)HFZ#)mHUg*(R{Ppeinq_7|(K0|g770;7i&!JG-Y!VDt6Zzp zSIv50M$4PRzP;EVe_xCW;kmOY&GSfj~Cjs6@U1%wt0OapmEBoqHV`uC1A|MrboHn!$z$PGKLI3#wU9K zJlxa=_7PLABVStizzIhA3VL)=D<{7-hAQMe=_Th~jj;d`EH5IJr>IaLI_)F1*=tzY zSKlQVJnd8o(iMcLL!LU0e&t418Utiq{8X`RCP1VY^Epg$nyk}g_N%Kl4e7RR>I$xL zj;5#s;)Ks2PTo?L*qtAoHr*+MW3F@Gm9X0Ad(DGQVUh=H!$Ucc+VN}hW@pZX7Oo3( zxfcx<#J&CXy-G`(j=#7zvpD}b_|sOh_TGwVthfzQojf+<)QngrN)?9GG}`Eex=d8O zc2q3SJ{fiI5~r9@@`po)uBT;sog7um(V>RKs$-=$tC+4#q0^xp`u!Q$EOCT^zUs!F z3zw%$WhQ%ufVmlra2o|gZ;g8cEGTaaQyw1a7Xmb>0IZ(|e!0LrZ&Q{!ZS)gt+;an0 zb#e08yB3h)52d!fl$5`e8NwyDOvDor%uw z*D@{NpW4Pa$;Gk7wZ;A>ltUi@r1fs%?85Z`Gp>hT8%(scwu{)TM1c6wV5Xake&9AX zKkqx^J5!U|EA8LgRn+0tzT}DSPxTn~V?=oRYm_AP>-sc58CW(k0WVT`j(K>$@BwII zYuS+FFSwD_%xl##nz&KQ5hHb54jX0bEg zsrg+U20+airv@pgPfi2X)CA@FpcifXmJiqZ#HjwUoSG()7%mX!jBfbS@gT(er2$v$ zb|G%$b$%lUrPox#hgIg>Ct!3FrirOq>Td`1+hfu`FGhAX)p~sX&?9xm>Uuv;Jm*B} z^(-t8e$LL=R0#n+3<6U-oU&KtMRVw=4OD@=OCEElgK{iw?47 z*Un%lq^vZ}Ks~<^+aE>FHSDX%+HW4_wO{4Og2B^}QIaKeiXKdV`k={szT&HYggCgA zZr*O~3cKySlIy!BRa-gx@KT7ZY}>;4=Ju5vOLp$@#CbH^%BH9J4?ue|gxB8Q1E<~k zWa%;O<;E<|csEs$SVhL-klC8iu6lS@)Ubk8DOtoIo|EkMo?3{x+t;Ww&ecOg& z%;?Q{l5Wkb+wHh(z|Ktt8Oxf{;ZFGOOTBU?iUtf;g&=yDkWlUcHgbSlozNn4n}JdF z4?}h{=?LW+v$pO!K{Woqa+&_QtMoKOO*Cd;`dqd@N~g1}r(S^0Z6iV&&N?`?oFDI- z2Lumm09$MAkIvC^aweT@01^BtVbCHxD_0pp8di`xi2B{#a~l3Xm!C9hz*{o-*JoaX zGDRSm!6qL$&7P-xJ-H<$y=aRinCe3my^wOwo84zq^9fuU|4_}3k;1p{+`BV|nCZUY za4&fJ<@3nS58$FiUQJ2lcJ0A;ER)3iOt5HIy(e9-5H~Mi88%MUev~}3C(g!@PMSRM zQ}{Y(xb!1BeW4mL6AxUkP+IIwRig>eiRaMZpV>Kn+?@nN2zgRq-%vAfTp(X;E(Et!W6;?Z5`FQt=Z26Ko@%R=;cB)BxDko_5xt?*JZj3xak>jC2CT7Cy5>Q@*uOd10Bj!X2|`%3{P%4eNIlQ_`B{_pjtZ$xd% zvQCqH-76=b<^rS_KDr~&kI5%4Fl^%RowN|2bzljxgV0H&URX^LKB0{a}+HBKCx z+K)AsS2;}dnr#o1#o3bXK3?AL)8A^RgR8wyRsrbpU!oYr^Z-_z0PY(1otzH!_f`38 zbmjr--@ZLw034;BY4Aa=?yBujzM3OcHfzB2p#Tt?$tgCg)cf{WHqT*Hd235$Kn3u9 z7AoE)X8W(m`yFm7*ev#n&2-sS->jd{veO^XB_R&AqEf zyM6~eq4W@5jP_siUs%4k0$g(InH1Yzn9HLvp0Udg{6@v)da@`{VcGWN00+e%eu3C= z8?ZKeIvy0lF=5At#%6)L7wiw8%0hVW$>iEbI}9E7=|{PmZ~y+jU2|+VkeS31v}dSS zYQ*>H)-A09b!WegdM*y7r&l0T1=)cdfQ6R`gtNOpx2xQC9OK>U71HR0+bE;9*%eal zDsAEPMy8K{q}g4irR8)+N%8J7j3j|hI$7@dV5Zpv502Qd*9>Js=peaoWx|muT^a+E!4!qkaEJ8ijpw}^u}hQ64pnV=1liq_W5_Gj zbSaf@-`duuJQjMnqi>FihCU4eqM{14d?su+>z*4mzDRXKRvB`tWjNq=v&nvQsUWMC z*NRSAD2MSi|c}AI1+i zPF}rw)osi)Ez~+MS!nl$MfCYojW^UR?9wNKElKM$lqbr*LcJpPL$^6uUD>vK8O^4A zht|YA^JK$)IV2@bnC*wPYB1y6d`4AC@B^%v;j>-8oGt&|74{=Fw}Ma$xPP_H7qY{k z!}}FYeQHM)wtc+qY9PD;OayS@-1^|6sVwXdsvEc3UP9hdKM>U^@CGh9KS}W z%}N`m3Hv=|79^OTZ9P#$l(NA1C+v~?Y8Z@Ky07AJKJ`12JORm*v?UrxHH8>Csq|La zH)+2{g*l9|ynthTV!iuS7<>4uO(A#c_dpkL*|NmhWBA~3M_ZsXk$ZR@|EDK;K}>r( zioP<}aQMe!NBGv(0VY@pYBr-3p~LOOAZ!(#Ho8vD*bun%(-PoYq8C!|W|DYo3CB+} zuPxgYmhIDbQn5?Nsky4D-HYxq+wV@Fhhz$@(^wF0X0(Wes?cIA=*{alsb+n_z{McuNUVLp_AXt`#fqrq zz!BD!*K}Fs;P3ve9>b72i(k(Q4J*?+b_qg{ML^wgLa_O+q@Jwdk4{bWPY#hq0`*%C zG*jzs)T$aKDQ4ZOB6VYylZDy^@tEe5!T!vn!^({sF5IuNlKD_^>h8CfbJPT|j-`Un z8r*ZXL{jl1M%f)msb`Um&;UcFtOu!6rMi`G0FQZ+D%9nQcU+obz2L=%<8a?kC3CStM{BZ^hH z1itaiAKDR{e!Ik2xYlncJHPLyN^(D@0l6yPdipvg>21=#XGMW<+tcQa-TF1@7f%=a zOs!wWk!=mBxTcuF^tQ05_4PldlW^DUN;T73%GOW3fu<~g3+5oTt1D;dW9a9sl^RspFf23JJgkNhc<+b{8()60KGj ze)EQ*U8|SJDW6)Q`5CqbbtKvmg6?tiM>g;`1eS_$#;45G&ceM<2KKVLp6?2IY-e5xNmd zej1X~dj7Z#d%`29AMRgLXOQY|(x(?wIM>(5svw9gNB%*5IRGh>RZksl5MDNM#<7aw zlg!LB71nZZE3u)8Bn~vC6H-3?-sqabc+3YG7v^lJ%lJJ;cH3gt=al-(3@G35 z=rn2^KZw-HeCR$z!J*El`}GNSBM4_I&23y7l}oB_>A!VXIk-}0=yfDr-#1l(NuNF4 ze6&8q<7DqQ9W0`3Y&F=neIu{u4JyFm>TeU&T^b<5Y5VcLeHH<7m*3QJ*-Y?gXB%IWcI(lq7kjCS=s4hwn zaDo1z#rviAZl)Q^!)ev4YX#W4nHZgVWqEo#+K!d+O*MV+P7t>i3F>35R(nJZ)k-0c zu_Myf%&OXcm{wv2RT#!!x^VmGll9Pd?*7sv695}J>P-^zNP%@I`TW<+`8$1BACZ6W zVV*Uq_v~^X-o|!ufoHV4#*R6gQ46u5s6w&kHsp7w)`0;m&@@^s? zN=1v5ar^tm5~47tiB8?DoDWQ>KqjdY0%(xvv2c-$VxM7m=E0Onw6=o+ zxU2xQ|C#&ytd@VpX~FdPV0d6A9p9Lp@;cZ5hM_b}F^&WVoDnfaXpJt%H2I7Ve5SXO z`ikB>uu;r5Bd6uRL$H|HoGN5j(c!WaD^xX#A2~ce4qC(*WqrGFDeZa~Dh;uAcqj1S z*czi*YsO?~Puu6AffQQ1jdKR)LwCgjmfr3NA#fO6Wv<{QE3m`#>S|+CS3Q4&hIDf$ zzOox zx9g=UEI8hG+HZz2`rfWOpKJ4WKKVDz?v?WK4OsH!U1p^A*KPC6PfduPBJ^i7Xh(l$v&ABRc6_gGotrAU z*q=wCXUGe_wWF(r^!zGKvvd$K#kXU`r;ipbXsVb^VBY@lAiE!@J6B~tY@!`wmgDKZ zoAh`;;xj-v^Pk_;JNeR4I)%^zXP0B0RvYJ3Qw5QuAK}#ss3K1YJkWLI_h-LdEoC96 zX(jbkdLiov;ASONc&5;F_RMId(`j!zgnUifO?hix-BWAKb z6go#YbMb8lnoD8o^!^PBh28n+8RR`)i9K}qQv|>;8t$|p;2Q=MYl;i!ca~MK0uSah zZV3EOSnB7oYUXxPTb7Vb)C{hQVC>MpK*<1-_6myM`8P$U%LT#CaDVuUH3DJ&5!Dv`;3+pZBk#NPA2gDkfd&!{#;fc*Q!|HIyU1~t{SZNq|qAW{?srK1!P z0qHe>q9RC9dJCXZr1zT8L_tMGdM{F?_f80i^b$IR5>QHj5P}2}dcN&--S0EE{{Lp) zcjoyc&cLj__gd##>nz7{oQ!KvbGoJ%jn4i6s=Vrv%!5v@OCvem%LgnLF%!i`0A0FyLf>5$(eL3TEg|6MWdQm*L1xDH^AWCG zh>le{fk!obMlt0s#5`5fo(CxH=H9k!y_exnpe)qQ3qRggRUS*dX4`)iD3TA3Rilk| z5Xv2z6(<$rg|koclQ^Fef$N6fAG=4;g*|lk-)_&JN}EEBWCk5R;PlTe9mMbSm-oU+ zr;d@s8RKsvnUUDL!E_0>gJzBITP;rNo#q`dlTCX$grJ#LuHa*&cWLz(?B;xP)L^kU zZmzL-ZQEVIetuV8SZ$W1-A!En{*H-IQ+II06;v@RWLQ>X<0R>9AS2+tei-eAJ5_Ez zBFy6Y1Tvu_K%Dw6y4)g{UPt*PE$|^QcHsw3&}2e+M#`?qhpS>5Gw_3i+OYtjl8F2Z zs;{!>Lj9ZF1V{w+0RE8kk!igP4^UWDIbQp~k~MvNn@jfWEfR z*Q`q{Pgw@V(V`LkJ6D99P%Ar zvD7UMFqm~@+fxcSEfnCAX}7Yr+MjWB?P>VMWIpA(&pq&o^+nt znin4$cOC$=H)N|Pd(|Dj*f`C!^vdzhBs_nBt2~NKyj__&B+(RIK3VPpdnm#}H>sDW zVV~K#K33pta=6sN5O_4J2uLj@Qqr=_a9wCUBCyRK;fkpL2y;UIq2e-Z4{<1f2MY~L zQyk4?ek-G-$hdB#s2+8)SR%6u&5>345q_1&V%sv!%Kkf-M>x%hyUR>b{jUx@bBE5> zI%fc0Z$oBXs1>Pop&uxB-$%1LR>%SJ9jV=oI>m;<)<<<((oq>)R;znkbB+B35yy(+ z?$*tN#ztbmR3MP%dKP|>#1Ek3D{WxNQ*UI8eVAIKSYdFrt(JAy2f>}qY8}WpDcV2TJN227p|Pz>NCIp|`Ued~|n1-aA#%zGV}TZ2Dbsq*FOc$->7QS;V-^E7jOI zfC#u*K4OC4Qc+IE)#E9yQFySmXl3`{yiYcsLvi2LOnI5%h@EdX-!H4G8Z}yr`l;vbe3JY8+R>x`b_C*GD!m+DjF}$tL69 z!|6DaTe)^Tg;nsGX&Y!nm{wok^F+Xw-P5UNA{e+TyMGg=oM#lBAYk}OT|K)vEE6b& z|3stF?jN_h>rQdL0XiP513#XA+PWj^~MiV7@g^;B(TY zGC_73rNQ&^3=KhRZZ_6`YD~So@tf@Dxze}dFf_{759G3H!AAg=B=&%oS;G{6E0>7E ztjTAH+h|Pr6zLak627QL)5^#R=;l9qCX@bSY{(rwb`r0|S|h){v(i75PeDPI!h?*y zpdD3)nxzSe<$G)!W(nhVo~a=qyxrsIh#oI=gNcWYEO~dga^Ed2rz$P#% zp_^Rcr{XV~(2BMNTa>iREGPX|0Zpz(?}vz^d5;Oq4okm7Ai}e$p%^h$s*z5_ZZ|E5 z#|d$P)YC5t;|4cD7tOo=1hz5Sk-Lji$h)Exbz7m<2=XOZQg zb3ktvHu zduymc5tqD3sR`HZtm2QXm)Gwsec}%p8!xec!KC@I*3;bkuAtq^@}VEjbNf8VChthh zj2UuxQ5PZrmlhHh6@!h=6c>ipwPvg1a$pqzpu4uChz&IT-9EYDVBgA_Vw=oN+U>U>=l2)T|e*S5I8up$}R?70^aL@Yt_7`clQ7aQXP-!xG_~|OYpzhk-Y_Y*7 z7y4odr|_7W!*oR`OebTm5qrL~X~#W`YA?@n0XOh|ycf&2)b;5y_`5o0kpd1L+y;YN zH1wLgxvBwjI_5fRACbclF{+`PW=fLv5Rm!q!-V(M$qhn>u zu1@pV>|#qYiSdAewTpGyhhd>2!+Sp>z~(D!4{<2iee^M>tlJZ+6komqYHc3TmhBQK z0ol!YF#RWD=BGxR>ujC#MauJ+-xePst;BhG?*mXgdqdlyo2RF&UX7y2%O>&L+in0( zq#;)v?=HpdjmRM@%*Sv_;|qH45P(I@BJ@|K z9%zp?r30vn3s1F$LxI^@9b2veZM0^1D%UwqxtKJ06~2zJtfPIs?vH)S2?!RAe|-u* zrG$uRw)5buD)@$pY5AE@WAD*TBK=l*;Xh;f)YU*t#s z;WM8B;y38I|C)o}>Fi5yfM_qQ2<$DrW}%cXI!4m|Swe z^vD(%so_u)?aW4QJA_3XEz7rM-IrfqN2pDAQBoZ~j%ia-=Va#hv47RMuz-+Z z)fW24)-J(R{Pu0|3oUMqP`P`%aOlO0(WDbR9?x`Ao_;DL%FW$&JVK0JgWo>G8}NHS zkglBJ{z7ZsaP@lkr_AUjfg!coxQdGYOU%sI(ORe_4{gFGNk=?ahv(W@LELh0%zX%l zz`s`gww#=GGyKgP^(300oX4!xjyIvmWA4pmTFJX^R#qW2?`gtwJeOq2|3_N)paqcs z5?Fal6Ojh?fQS%blfP~*WuxhztRM`^4oKl1mCr_)_?M(ZSZot6rgn+@;m*q-I25L&S>#l z=MyAkQCTin>HBr-an#&}LSBztazyWSg+DyIl7<|6+$1SzP5Kcl{TbDY`MTU6D>iA< zx!St?FS%yXmQr#bX8p?2E(kZ6uoRR|HJW(u+rZaC?#V8ykq75(8}mY>-mG5~i4&~`Uw(|HaYV`&{6{5P1$wE0sl9wmiSDaZMXOVg zF(4Nlf&*A0N2e0`ZXT{LOhjhb`H5ib}Se*S$BshWEr$2JAR8Hge5@N0 zIDl~(CnRJu!~V_pgwQ3ON1?pCsJlgycb}{eT3pmGc?-i7x7JOaH)a$%toPjvIsX!h&J}vC4ySV!J_z;@Kel!a9CtvKT4JiXM&_)pL527iU zr5#g1gAgMp39wy7%OmtS|2cs}?_C*2A5$shUy3)h1IW4*K%|87}l}pw?;E^7I#*}z-E3Lz{MXL*P{oWln|6y9VeWO{!2jEix zh=i~wS~!-HHZisP*dHjCHQC1Dd})gO1$;&wmq z7Dd*(Yw(q1CDBJar`CpiwY$dKnBrT(5!3hMp_~FwSTV% zc2%;IGqUCOGXrm`Zrhg<0r}q8TniYh$HU?-RLh)`byAyxd`eLEobgktP&cMU(xzMU ziyGoF9+KLB{x+3wKM!0V-~I*RBC;@|RV9Wmri8@2wM;AeCS{jl32 z{HPX!2x2>CmO?ZslhP(VPLa;MLrwz8A$~Yi%N~WD^)hy@z3vAT*v-Mf33WW{&P#sx zqO4n+1dE{97vnNS0j*!l_C2m^`(2M4mAb%BjKU_pxa9`Z2c`ct*}F``lodNMgspke zU$*HVRtGm8H&*54nK$a{!nNr}OzT|wXK$SD1Z=g`#8v_J1Esu59tb^=Qy`Z+pPTFd z>Ip|m+N{@vLE>!o0k|jUrBew=X+`$7Qp~JlF8e>GXid8;e0-(QqBQ%&qu6PtYQDA9 zl+QJaLD8wh)>jKKd6iu1;j`?5zA>1$#-SfBAe70TKYvApJMKn%+T@}GPYu4Ks6{UP zwjS=U`22!f;{~TB=lu*b_zy-(xC)jlzZa=U_v|aZV z9>&XL1!$QhSmnA}Ku~(dVwU+$fqz}$Ha^BHE2pxPwKpOLy)pGY-+S@A($c0PN^lt6 zD^FS8u`OtZydKCJ*o12|z;^`@Uo-P8-e6>5y0N}q=)ujW^}c)AnL1JDfxq9zUV&M(jYgaigWm=={dbUdwH4Sr{NI zMy&-b_;8D8&3UqN73jSZ-qc_UUam*A2=v~TY&hG|phSoiUy|p4nB}sZuUV-gwCe)obUeMT5?w#~}Sy6h|v-c8B^trw(|Ll&}Z&bXZTZ9cfJ;GA5E#E|E z=iq&|zR3!B1C#DXu@6^}9ZI7Nh0?CwDeZAr1ccN(FE}Ee)_G;$ zp}38hWB`+D{Fye+E+a(A@Mv>7%mW>|{kKR4hoMnmJSTkElYuRQbozixd` zREQFy{*lG%&N7;J^(rVLBNEWIj}``^1tc-CIPy*?>qP$}P0fqQS_LFkJCJ`8i4z)M z^qtB$d&zmD%%s%m%eXqC75i0`9=*;a^1EE*)F~R zF;XF)j8Iwg#f5^gC-qGAF@E)dJ;kSHz|f>!=Vz{1W1G@_WNWC}R|ap5Tp3{hk?r1i zWNFIfom&i2ptV@9u)`kk?Sr=J6RG|g+nQ&QqP?)t-s9`%hbsU4)&}Luyk>oaF_gUV zXv%}aLN@WWiY)C*9k;N6cjxc)!Cro6svMe$7+ zSg_l!Y#$%C-N>SS5iiK$O1>CaHBZrY_%?S=kzYLmvhEqQ_f&27aA)kFh69W)jFH4zp7mKUjqZ&pL-}m{?E1lKmTwG z02@E;^>oyqAspjHz`wcZ&ell%XH*uN_IoE@dj4mxix8FjzbECp^YZn-f<})^+-HW= z-=6&Y&HoeBUIgL<_KWq=k$(k)J?g-M{r~>*!N%vEkWB`shn{gj>dt6%Y|LhGrgmpg zChP%ToqGA96_9}u_geF<^8593?sCBeV2&BZ$pLe&a_R(tfVS1_vPrkj>FL@G9j^e# z>Ck8U7kAk_*0!ISw0M1l4ZWK5&Sr#A` z9oq_wh)M+V(!GGPU*6r5!_i8;bN*{^hDShY>AW#yGNK?iw+CClz?btd>KC(Vja$at z@R2P5x=A+o`1N21NZR-j-*j|=edmRQj7saYoaff8{ZICcpeZol1*LT)o;9;&?fQdU z+dsH?uX)Q-*IpW!kO~UpwRX!yp6j~NN_zcewiGy?T14{9!11X?c zTbZv5DqF2b^y@RVk$ShYEiJiZpC}z~>j_%6rVS)1J$U}&#f`&&Mp+8Rn~I?|H5ZcQ zJ)@nFRUH(W_5@$IU$6b_XY(uLL?N4}R2x@5WJ>BxmMI@T!(A&#cT%E20cc~~=Le4s z`{O}PkG#BI5`&L-x-TO$p?4PPMwIP9A{#f=g{Jxk+2~bvwn$`j71ROXs+& zS8q!Z@8^0RG3!>|vnO3Aoa`8|*{p8znIw!cu*qT_AjZ}e>E&sj<+a#GhUfVV@Tr*n$R<@eWr@y}r+_~TjIdWEZ@)5K_oDs-t3rQ4qE>L? z7Bca6@P`zhy$${N$pR^P{*N=2o>4m6Y)nkT6Gd@}%gdM9&UANny@qZV-vM-hBy;M? zNgqc~u0_zRH{RgleaW4glyn4F6Uvk8H_kep_M;QzvlAvi1nrT^v7R_xgb1<-L66#LXP55q~S_!_d82>ems#q2xDL zP+`9UNVGv;EeMsj+)I&kxgN`}6{AdIvsDV|nXs9ne98zvX1}0+L!ks=A!J{4m0R^y`PVKWGY&^juY% zb0HmKa2{h?GS+=lA3PCqW`O9LzF~>6eK;=2H5^`A32Q*71tyq#8z4fu_w8DbwvNOD zJ`|jT84jl8pF3bh*1MKNNga0pL7`YEah_qc@P%6A@eb!{goZ_#XuXZ6o`{UiE?1Ve zPD5}_4y6g*iMx zRysy)PB%_Lzw*b_JP(unY6R|!6Nz5Ghu?wlJwaku*|i-evRl*Uvho9GTkJUsE$dTw zzq5GzaN&(Ovdx!!)^4D!LuWQOX?Kg@)u~cSf@5XIOg%<7Ej0psaMmQ@i(l0g%F%sZ zF^aag&00nD&H&)jBJ+uu*}By_^Q_pSk@!UKi*&1zS3c6}e_vhk`jkenGZ9l@q~-zc z2*##s(Zg{7;%sUbKTCRpUJhp~tH0Y_T94vAYZ34O6@cL|u?djsj>=r*&}jDSy)>4y z{~imTt9OM~Le%HnP1iK9HPuM}oQK?K`w+#sU%Ps?HK-GF$ln1DlWbCdVm1t%vshZY02?8(G z3_X~bs&e~r&wi?|^xC(Ry5nEnnbKBMXKQ?)%;v98&TdI|Yb63j!2bFL=(?UZL~X$j z@2s#3*-A^+ZSu00*gQOY?bnh{)yt9VvrI?uZq9fV3_^L#*p{)_BSZIG%AUJP*&psS zTQ^UYWkT6D(^1Nw(&P)*pYdIDs_*`EqF_X}{hQMcu!AbP*W`kaumZC|gAFT_sd*0~ z$2MDalnw^i&Nya~+Rt(IZC|_roAUkfhNb5$aY3NgVWMi~h{(mQIsl!C)s|Q11a1NX zyIz4-tnFY*Z*lpB&-q%Z%Y|vos7B9%mg7Fhox}sH2WRo}-W!idEE#)*#hLSiGO!37 zjPwPDn4v}Gdpr8?6n0}PjzwGdH7wRh*?Rh0Ef`J!1stP!Rrz6>G-lY2gB+W?5KK}< zkrr>5Wv+T}j6Xf~TmX&MIkTC4Kas#6Wa%tpeNi%>;Im)GMHPu9=Fr{;+6FDxLsVYV z+%~D3r1zf$65_X>ipba362t0qSJ+xqZP6+INnm-&dZSDz_XGY@i3O^j8HIul``Rf( z$h&l&+7|)?(H~QNlG^wlYCBsjT=)UDMKH0j2y@J&T{pOu82?W1R7y2n#{YrcAeg@z zEu83Bwsvdimnm@~(YMxyfK}!WPPGa|picOo2ww+4B9deQ;U(bu!8((uv1irFCrZeK zDXW?FA8GP0qHkL^jwE2*)gkJS2W3smg1q&*4QKJl(HeuOvBoBni0Mjgp>b=Cm(#-S zSrttkRyH=_!4zxy5bw-MIY*ZmXVocf;oO~Qp5X(z#G!ZTcl=wOrW@wRp~iqT8()0Y zRn}58ecZ0=kET7d+<@BmKEw=AZ{3vg6O+L)Phs(?3U7?atWYtR_V`zeyV?r1UqIg> zfvfZD!a50j>Ws!LsVlDG^nl|3Z_tc|jhfvR*wjmb=dN!aOW66ZMs92` zS-{Gw;a~SINqvSLl_}(!P81Q|KaSgm?tlPJQDg3`4S}Ur^$XdWDuc4a*~xCM-~n4# zR1K2A8H^vS&xQ>gMuNxQUR3VOeiLcEISm&VcFTheD%9b2<$}=k2nhHx=38WwKPFks zGk?zKD1DVd<<&ZNPE^4cTh+~>=r^2Uw6K^)zT&n6 z#s>#zu0gKdja4pVNNHlqJQU+>!Nt)Oh&o{D-9(NJDwdpY6s@<5@>&n5BcfiiC5l?Q zM2G`DuY^Yn5My=7-;kP<&mHs99M{V|zXu=qzrMztwCNnlnMLdp#MB@Sy_QplzHH6n z}^0oT#89?`HOyRJ(P2toNJ!Iu#T_Q=u#dVgdSeWi*=tDgoU0DCQzH z?L&S--xentmAe?QUEX*UyMF^z*`zY@HdEl>SX<6(t%9Sg#ra62aW|WOHEi@cf175# z4L!SNZ0{IziZg?85shN^hfLvl5hpwHPm->!TNC0y#92j|z9b>8cy2>3=rr(seSv&O zoEgkE*1p_xF4`mi652O=5|^tl@!A-lE{er^_X$gLk*_sMtz&AgMM?6v*`f(uhhUz&A&t)<$2uHJ?5S@??E$KD$7hWt>de-Bm zr^@kKQRPf$5M~Y6`rElv|YbP-U~7>T3&LDOa7V*ma@ zMccp$4-=$gx>+4IVc|55Kha_iRgmT>>P=lgS&gDtI^4(wHxv5HgEebGT`2`z76XrRlKrVwz-+%CC z!4+#G^zi@lr}7Q$}%{e zE=a291c1A3?yb4`Vt)l;NT>>fmuCEme-ogZLWE^z zlA?jUqBx~bDoREmUPx~gx!bzO6!>fT{0?b_!>XA25dQyqg8guWk(Eo|SqCs97G zd_|PgHGqO`hSSz=;g-ZyjcTf`QpX!_YOkorijJ(ZD``gH3;iZE4M`>qgUy?p1}3 zZvb6Cct~U&VYOGZGKjDZoW+#cNPV3^S!>}*V0+8GRDx2y%S6vOF5~CQLHo#?_B1b( zdU_B4dTpQSzz>4W&teH>v5eGofZCqwlG6SALc3APgbauGQlK!oJWxIh!lXsr$j&GH zGs@|P|#1aSpHetxti z10av*{l1^0e&H4%P*|^;19TGdKKk*7UWcFqpg*Ehq>3IhNo*c;bE(bD<5v;Yu(_=& zWc(3ij>1eIH2xIaA1{OkK?x;twl}L!Uku^Z4wxl!PfMzYYL50gVu4Ej%)#OzUbjip z9_QX2$Fa4I?aUjPd2uz7_rg;Mq@Cg$(X4`?qo2vTS496 zz*^d+d~{8TpbRo$jMG3EeIa>rVE_4p>NGqi&8WXt8ZzPS@0}RMM~yE zu8_Fz`t&!>EsEU3hhd`zj*54mjWk?UU=Ki`mK8Hshoeyzq_Z+jj*-P^VyeHntA_)a*IXJBzl&~^7LlHjG7*H z4rsSHXnJ#)Jl@;UDE=WGD5tf~dEo^)VV=+dj&>Fyie`y@p=j42XB`K z)b0@Ei&J<`PVw!|Jgq!#X%rr(EL-jlR}1Kr$xbAnpr4f)SKdpwZIZH1zImMFjd5zF z4@Fh2tO>kfU6d<~E=E5!680&u)k>Yw`6N|tTAPDcKaP0R7)4W)UZdD+ipXfXHeWy`;y^HY+9QWmNhCO7S}SA32&P{f=ylEY9%zGd=lx) zCzdZnxma^lqq%H7qq>Nl(Qr+i$}ViazS3N<%=4 zDHBud${D+y%iN{HvN*c4^(M~?P6bbqtniH)WBSoDHMQASYQBEW6p$Uxb~v?P>iXz& zvnqC>DTd+cD>X@sabdRv(`-O zRpny!tl&96+Dbr1>6vg=@kNCw9MbVs^CiAZ&@wg1I6X&S1uR%zZQA64ahQRCm5B4s zo^MG|kngtDd&DomdMuAjH(t&UEnIHC!LpDh?J%Oj|9LVO4Y0r43D0LfK!b{nf2~El z2gJKhAh~y&f*ND%%z{Y@u=uUOrczk8JbWtsk|rbb%@uURo}cLTb1`wcs4s9qfz)l{i7lcMQt$VN{A+=Gqd`R+`=zE+&$ z>PsJtPo;0B#FT+VLbIWV&pCC}w>K3}a~^6bm_zcD!%8w^HtwmhYOW_L$dR~5T7*+U z?H{Rj`ilj2?#!KuE{l}zS)m%13Ke$d^*#NYE zPzL!$9Uwi|!jiWfw*}c3we+UWUE~VfDKvNcae#WJJ);0UrDQ=;7Sx=Bv>(g){1Pi* z4YZDB+FoyQ(mD*i=MoT;F7I(-p;+BkS=#>D{Ai=2ims3@KEcDRb=~1q$3+9ow)MT<2T7Bd}zX zDc(Q$eZ|EpWg5f2&O<&?)uG7Ar00sYF-w_6hD!3fYLRNm8QmqXEa$m6^mwNb#6eu+ z?k^REOdyUge|=c84Zu=!I{>Zeh`Ky%6R>c@2)G%->gfge%ijdO09su6w) zy`Y8p-m&<>@`jq&64Pen?U}lHrE_KxJD3P|d9RZ`PHSi1ZBdOMpheFRT)D&Yk3SfucC_wO>bLu?m$Dq89VMJ6ukb$jVp#K$<=Aj{h z^Lgfy78D@l>J9J!Uenzn5%*(JigruO57i*r3qeFsvb=Xn%i#oKm@#PEAb*`VF#SY{Z%)%+N zJY$DF1uYbC(+Aj@+U4v>k&4>&JG*SIEd~#xSYJfkT7dRrf2&A9NFtb#`Jk6>UDxc` z<*Jj8q^FOg`x!ZyvCyoMIxRNb>9@I;Q>~??+K3!w9oD%G5gF@d^)(Qt%;D57L#~m2pI|7(ltc#MhMfHt8RJXAtHfkF|me8{yKkKyZ>9KferBY;5}HFQ2-k>wsb);O_ERPBQ1Jz)*0Kro5euD1%<1S< zE@HXVqyu00RVkGyU9C|?@Q{z%bngQP?!`B9Heu3k)R+6B+?y}3NzY{%g=q!ej{<&N zb&ia!iFgY6vL~j%%4p$p5MP%@m46EB91`0y!mdnqewBQJA#UPVe<`1JJ{gii1(RS~ zKYbvp116{@NRrAJY(Jj)sszFH;1J`Abu;C-1+KPT)Jc*sNbCQA&cHgp>g20qvCKTD z2&9`Kxns6;i6?C}Pu~q-3E|3WJ4N$&0qGm2f z9BhmrP5EUdGnr5BhA-UAgv$5S;Wj-LLt&mAGP|v)x`M)MhPYh2Ovv3<@w&sSyrkTY zj`YJ_yDxV;y6WEQEzlyA(I=|7;M=&1DvRG4hr0=D%EtoQCRU=xVGl{qYHT!zM{}P# zH=R^wG6Af z0idv3qN)%=1jUa1iemG)6SE|}oW7JdZMTvZA4hCDMByuefm_#}-5yt}lJ8;q)C=(+dJFvDe zrIYD0iqiH_d>+iJX{M^IX)`xcq;ssq$Os-sLtAHcohUxL@Cl}`XmLY2^xD~fcd7l6=Pm%Xj8sQYF>X6J`) z%59)Uaw5IyxCrTiPVuV>#0O!gM_Vwnprbtj2`B3a5L(LN&#BAufi;yC(Gx@ zi%isqHbzIO47Q5L7d%<_E!>7iq9s_N-dMjPy0>#S-kjT=EH&PgPeOxkaDcv@K)QSP zPWF}{EvJJm-xcN6BX}`4ucj9l-F1)GSV`-Em4oG_Wk7LiVJz5MALTIv1h#CRnBIf5 zs}tu6FxsX`l04&N^{SKJ`BT}KMbH=IwsSK?hcN4}G!-45IkqSH)KkE|9NlYj!P2L% z!7^vSGfqlMn%2OCL9d+j(`IW2JDc$nR?m98aga6vpI=FH<}g{@0#Bm0=xxO$Nx4G) zj^(geuaP1=f^gho;~SN>bGT9GTeo-IROIWfmw8Gzb}*DallfDAKo6oemT$^GJ-M`t zD`Yy}Kp>OTwY}r*m_nC?7~|vOyo`qC^F4v#Z8NB8L48X;?Rak0p2O6q)Z~NX`p-Pu%Bb|wobJ3;hNm^xGHSso zGQFqEqSr_cxnr?-CXGHca|g8ZlYkb7tE9v z!TSA${en5=_V(+mXvfq8mFq)wqQ&A0K2G(;s*YBf24*!%;AzKH|JyNT*Gr>_JLL$u z$#mTE`mZf$pw2(An&C4lEFGh7Qfqu=o4xMv8R%-dTF{KK`Ii_COvn|b)^FZ{dH3qV zEu@<7Ri7qv$RE4T3YEZf%CP?VI-U$kl#MmrBiHCL)emK=zD#m zk28cicg<6xuzAKKX_?m1xUNR}ah}e@_D5Oxb-Y*lL{f8#L2Yr0+T-=Wv8pYGlUh3> zdI3}%icxgr&`nR-SidrPWN_7fDW4<%TyXjKB=j`qMS`TT7y^i!O z6S;@BL^tDQ5W*dVO6IF#qxGI&A@z9i^cbGuZ2}&YrgODqZR%MpRJfL!C z2C1ulZ2D07tGlitdiMFDSwVA|dS!LGC1E7% zIJ1xz`z$h{EWyK8WWz$@q&vCb+RKI_ku-zPxuMncQuGh|aE)_O(dQp3)J8~)_QHmZ ze7dg~E@IMNtfuAJQrFrABmvFkCy(jI)nw%g4L=T$w1(X3rJ9%Rk5)+vX1nGIIE zrK|0%NjRNwfxKE~di_;ZRg5I2p6#UURzAv*`;oyw)Nw@)4>2)(Oc*^+;UNIfl;`B0 z-ns{X9g#Qs2gnWBCSBt^^3OF@`ct`U!`@xgXa@jt001M+io9tpR^&R*+2weq3zd( zA*o5Zr6E?%Ix?s#HwA@<1VzdjSqhiST7O|JZYHU?kXwbTunZ*S{6#?cb{~LYXu4b; z{EIV;O`vq%vHWmE^DnT-D?rPeWgzN*hyUNn`CrQ+qxgSj zTTy_{wEhTB&3jQGVKba13N=0LVCZme-5M+J7irdQJ@I*4KQN|4tULfpM}*b&aL>6G ze|+E$0={_3f9;1_;7*#%`uciZ8JQ^Tr}eMDdUi1WAiql&k|<)vAN-M3xIa-eF-rMh zAOesggh|5zae>j+Kr?mI3||{;SXx)#6_jbV8M> zi2gYF1psl{SH}W5pg$01Dz9lNq|Tfk6jz{efC?ED=_e=! zX2jr!;Ex`DI#vfXzheq%o<4ngiG@XgPb2R0gyZDf;BwdevmQNHm4mW@ZfsgLsDy%y zTL8qb?+3Nd-`KnqmEnL8^g!|rw!u5A8KCmdK$cviG)2m0`}3EW=;$9W zWIu4mZS4YWAOVef3;5Kan+>*b!eXIc6vq>IwEJtT-$j+B4v5n^Q2;ksJttPj=l%9y z%jforhJx>mwhrR6YwI;9`$S=_OD7u2qj-g%-O);9+l#L77$6_20fn~Ey_UK#72ljG(`H)=pd+UYfO;1MGztE1~Ou26f3a-1&H+e^` zKaee!RR}FOvTj23`nopvule|R%N~u5j#`sWPkv)gT+@e?0=XU*T5Ejj?;)Bk7ooxe6x10bdnfl1Gq|6_lhA@~0c z>||nj^Vf5~0hw4?a~wPNFU6UjSd}N7VaqNf86d$bWjTXU-LaJl2oUA|4^YFhtgYOrAf_zO8NUlL+Dq3^(-HrPayYc|61DfcSFuJFftbY4IthHSZk^z&XIe6jdKs+fQnGuFZ$Ck z8L0z9KhuSfSfvbst`n?PuSl91x9U*=Q94KDD+ia|V99cxb+;{A#^d$;QqM7P#sPPT z2+&zXutfv^bZnm{7QiGa^>jyPsNOrVYa@O0!OjQ=pURm6(C=g4U;lEN{&TL}Q{m=4 zyQmt$(9aa5ER1J4PURcfkaEn^SvZY>E@sthW+~!^?xC*AxMauOG?^=yRPk4$GrX8IEReZ*qxUGbTBu9uzvn|%o1Ev`nc_F^LaVg^vd3r3pi={Sc3D;@ioX83 z81l`uW5i|PfUMP_3*CX9XU~xV5RlPwo~uBRZ<_aB^U$tlw`t4474>V{@8>%;n}H-t zUJ(&#%L2r@9k*pX=Tg|W%E9iW*Bk$}X4Kb0lRn$4hlFoVx9Vj;i!yH~k;)}q-QUl7 zE$w-Xs@L9!F#iU@LB7uu6>pZBX51=8$O`MBfJ_T_NBGCMMm+PQ<~qBc?TgC2T>xTv z)3d1$sKjp24NSUDa|3zcI2tAaprOgi$}2n~o>o*5F{$j@PHxh~t6tX7(p>+BP1+@n zE}s)r5KF$ICAmk(xszUE+Bj233O%=%&;Gt?!l7bDRmmB(OD{Ic2K#BqL7VTRUdfrCDc- zgaD5gO%&GA7YI8}mBcpfX~!twnzUHQi_xVEu;}=1ay=4|8jClw#EQlX8C69b6Tfr= zGPYGayLwSb;>O-iNa)Utqv(68RiI`a!*WEtf*pbV>QY!vaSZ6Q4E|U(HB#ox)_9bm zund;a3;^^KbaNg`K`dFNOK>iqd=(UOPV>Ql^hcsF(4%EpP|x|urM0m>>c_@}n8a;Uz4WHql{rsD-K>%M zBzQ=^+OthjxWWb_eGL>)(xS92ygr&2Yd&*w>6*B8lH$pLC&BN;hX`iq*{31(Oc;TA z_ApbifHxE?s8=w1!WAspLlxNUiz6gt%6o^$30VS|)uLn@)WT9%e;9+(t!vk=c}z?s z{#F87sp>C_ufe#dk@L*gj(<^ASf3&azI%VE(6aoK;&xk36>7J2@~Z`SrbZ5| zbl~?n&EL>Y(2iU|D|APav>1A}E)n}-%xv9E@M-pIpxSL2%Em9u#(r`xTKF zLGwwfRzfuulVqhc))6%)>ZoO9UZgL0WC7Po76;cjZ!KpM3A0>Db>33#p2}mVHyf+a zS3D+>EWHfMA3mSkf=>m0l?0Tw4;j`3>@txP3E1&MSXwYDPyEHq?e)3Fu-o9K@c|hT zL1GeW6yPm5Ar`~wQk{?@=?cB|3qh`7zi5Y)yC;ibAGdb5nz{Py3+i`GE&P5Q?CkWP zbzUq4MQzn6`v)g*0*-#{r+nvF%`+vSpG|9tR86erfc|TD-DBlPYfefpdv2w>&g^e@ z5NGGr)%$HGOBxBN@10%hwM7OiUm`dYooDMKsW4k{vC#<=416dk-eBwnt1!69CuVED zOcK;Dk*n8D5MRTW^GQp08Pml~CmBn8Kb0SYoW6w;4vhHy_Mk~Wl`LKAxdPV68rQyg zI=h3XOStVTO=?PV>@nV}s`Od`h@X+nLXYCv|LBDw68f1wL~4I*(I}Qabh4)Fc96l~ zfcv1M!^oUx3+GfT%x=qNv}#4S+R@JbMYtMf7-c_Zo8k|%dt}7a7B7OO@pCjR=Q&*A z#d=Si*-kDXp{x`WkGgDax z!%PeYW6XWHuJ7-AUHLt@U;hX9>wfZo<~5D!obx!H=jV8@bYkp50p|uF*haa7!h)45 z)54g=!PLmfazTquWm2{%;34Bv3KfI6yI0KMS^;|l0k{s6%k3(b6W|`vtqA?!f%J!$-8z3EPT1TRa!6)XIA@>y!5QnCqi>_ zUarimX&S6453yd+u$tDJR7{Xlu_d#?FLkBwqc$A*EX047163coi7*V6*vXN8`sd*LHVY`1jF8n*0u}R= z{kOcyBQaXKbT-n|;r@QLPX(xwu5E)>tNlv}agOJ0(ld9#6_iy*IU(?e=i!`B8~>hggkibsIkQ z_vVf7>5Ua{qW!8AA|^7t8|Btjy&8vX;Hhx0Mr|-qchPzL$61}GS zgM&W@l=-|)ItcBmzQ(uedu*lebRRkc*22|gZwfNZ7L+fF$1YBG=;eNWIcH-QFnF!n zE#qii6$cYVz5dPawt&6OxAWq;nb^W6&9qh{b^Bx2C45EP2H)VGweZe;@MVnyb(jMj z8z5LsFb?QFw6oBecZ@Oo1Azo$nD=@7*lfqClAw>M6w$R64f4jXDNKgd=cO0&u=N?z zWQ8&MgJvqF9%#S**~La|?0e+Tb3ZWk)|-be3X^Y$%sb{s>^!6Qbok9a7{>(7?a-^& zUw~6X&vBhpjC&%k^r3ZSHlPB7`{0^5KQeGMTY;@_Y0`rD>S!XhMjNrr1fl(Nr|#kd zOf06wL7QHkZh2$LYM-uq>Q`hJss3C{p4N_uy(G8^4n| z5it=jKDVGKmrC3S3sec5yQ3j;+!-E#UJw4z0H!WNIvD7?I`H%kQ)5l{V>m0%(b}sC zhT%3tA>XyG{QI2$d1&tp+ikwsMdO=4+l%c=(*>H=*Ipwr&#~P&`-nw<9Z%Y{NES4B z*-WWTQVB2G5}g{yF0Jkk-$5QKaxm9zvH(x_Pg3bia}_)1^n2G=`60#TrO810I6C+! zAb&h<>Z3-fwNU8B&DUz`6flG&6(cF%Bu&*6Ho=Q$(YxY2Owb2^e5T9w5J?#A<^U(27G zo(WATJ5{P3%2){{a90hr?o)S~`h)z|RHYEs{?jQirbI9)F)F2(?Y1e0oX()03e?OL zE~lwnqlYA_i>P#5{FUDxit}@yP{z_pEonR@j3pl(W3{z+FBBx~i9HO(o5iL^u5RZ2 zh}%Ip9&Z0+XBX^;JxO>-0Jqg5l z);w_Uv!fxV4w+}KjW$jUWl&{{5f-T~*|0I|CtUyD=KxY6X~s##rp62Za4_L>Z}IS) z{HGL`n?0iz3ui?dI}N<+U1SD9JK5zNErir_ zKc_OuLk`txZyJ&%%w)Cga~m(e!oE@WKZh?nr3RaoucKzO(g$N(AMknYPa;)XK+&6y(3Ne zOSauz*7|Y-qicURgHEaG&I}MjHpaS5b04o*Ymuov~YqHiWWEZ8$ zNkP!UshKJ_0TnB)NFzGJwxP>1&Y;uH7m=byi&nQ?#L6gV^)oBRi!cs(bUn?6p5P5~ zPfJT>p_PT=Fd=|sgIxeFn*U;T2RnA0RGd>Zg{bGcM&hxD&a!?fJJG#JYYoFnr&T#F z^=NMFoSeypZJ54RljQXdHd~gs(dXbld53kxGx%<9^nc<@$U^)^`g(CV9Bwr}JZ%Ed*(5&Uy03sgv(E zGZ%eFkpZZ!ZyIXJzm^)%K`vGWU$i^u^+XdAiy-FYchSF`CpFaLer%5TP`fD_EmvC| zM>Gn#0gw03AEj{Ej?*XENK{{0A+wH)YPafq^Se|RSH6+My$!zlT%7YJE*kxl+KpbuPZS&#dtc0WAKM*i&3<>2-ov-W>K z@X&>W+%nJ`JHCIbJ+22ai0#33{Y~Bd_Z=Wqn$N`^eZ%PTzHnaNpCZc23P}R@wq;Li zvG00Q3hb^EOsUkJfLiBK$gh#pUbKm&vQEui~g{0aC*E*gyDP z5#TO`iv0G*ty6jaV3Kn4rv_o{_;`YOSgUQ%-9NJo0pPD4je?Ka=PNWEUUH9?t z1z&6tPkBMKMy++n?rjIT>o9=A@~#N}1{H>DeF3zl&Y0xn!)*t-1H8^*a(WxCE&LPU za%U+izL&QhWCVD9XHnetk~p6&=Y5bto$&AHI6@1&9&xKp`=4{Q_0!W!;PgI4<=Y(G zc96mmPdUSR1l2%Ks^otE(%l`_K;;Ki6DX~K``bX?<^|rp!E9qFI;8K7g9X3*bo3RJ zTd4tg47gky7N)GhMvfG{r(_K>Gs73ce<@BVge%kj{R4kQ>Wv=v<%pa>|a|=>|Pw|^Uw#W z#{EOUlXYg;CN%%RgN5BiX2s@;I*w@f?re461yOYnNX;?wa0{bX+S7C)XbfS!!ZCH% z+sf^|CoM)mb*EsJ6WJWa`t>6m)xHhvY|GU$6zt@Cx7w}Aw2$=$CebZh^~Kx2kn*0R zU#6Igo!$WFL$`A^B0*8DPTQx*^NZiZsHy1+ZckU*2(0GFzBi$8J%xzzZy&=tf!{{j z!p0}cX6UYykYl&Olr5o8>`b{J0u%db)h zOcUB5hNR-v@TTSflL8+>xggbkeGi+Mc)}GrI>QoER<-} zqHnw2b|2hp#TN3--wDCQ)BsDm(@a078Z0+F7PC1=Hv#_Q3sJyGse){&d;607q$a2^Z zm+7F8OiHf6I3A=hc1~K@08gcY9J`MI*nb+7`wr^ECMVey_YUdjI@pUyDH?GLX*hNx zaa(dP&5b{7D2(o%CJhhXX8M~50I-eWI6F|Z_;v;sh-Ia^LL2Ls=K@}A%((oO+$~|= zRai<<3L=K)G!|GXxOY6-$Lk16mnr!F1b6-So_^$p@ICFN| zcPhB~?dq$TDXPN378>a$hQ#WI%R8&Uh^4qs>=i;2yy;e#_2%oL1 z++1ri^`DK2tPm0B@F&&>9+;2V19p=Fr|1lAh`uG<0w(bg#jEXC+W zf%43RgfqFA`?Ev@!&VG(O1v{st?@H(AtlK;Y*ur+%z#?Ag@x?8`mH!;DO;6j(^$M~ zQ&tc`d2t9w9j(rE#Ijmap(EAuZvN#(+{r)ZXvw!h#>J#*7H?Fy4z-pM#Q^InX%#oA9^m2)ds;Ve|%0*VX zSvFK?wZPVp48lsvwVy;EQTF98z?nsfsDjdIlSPscdi2N87EM2IHqZLg&-usuQD0(c z5Dr$I3iU~z=kgltlXJ9;9lSdP;2-whR>c{63yMkr3sg`J@1a3o!O+a?P=eH>^PMp8dcMT~k+n4Xyo)8g8k}%G>Z1ZY)viQ)c z-zShqY0XdL!&WP6f!Z#FK+br5AOh<@bV}AgyA+2us*^lFiA)j za|Sr2`X4I!(`Xm^v7~@>o<){htH)>!;I_2bZpWSees{XSNq5q@+tlX{SH=x^0+p z6g}t^_@$*q_sW&bq>E|HvyK(-&wg9G%o*O_Ckyi99BCoMbJ4tf8Ru?ZxW0XEQ~f_+ zcezkG-Y0&qHhHjHzM%h`k%QXHwx+hB~Yz5g3>2Exr8_uuOZ`mSZA3$+2C2%37O6z}!F)IO29?aRlX z10}p}`oX$TtF8CVrzxXPGySx-alkwFJ<{&tOJb|ldVcEn;|KrMX^^nQx8sc2_NCg2 z{+cG9wc2(AyRmm`sls)SZdi=qx*4^p{`T>< z&v^_&cWw4Qy#;J&|NfVQ>pR5#**1pXeFT@UIXty#b@Alda*bx}h`>qBy9o_UEJtez zqVOk}>g^--n28ju>lZn57-qrHveJ9z3R6=eGvWWe%VV}?|n+O*E z{u&wC``8z0fwYteU0+F^*vZkj{PLvIO#tQCnV9xBz*_AcdVl82&P5$6cb>1mSR6)s z88jJ7WOevzs{S^$1LFPC-JN$>8Ok23HH)*w((5iHG80Ub6RA02$vZg`8e28jCURv1 z=a4|%L&m5cJIR+B1|k+SA4n8=j+O(F}cYYoHy6;N8HDM)epQk(=MY1yi`Xl&nheemqsUuT*&iH*6*tz zP5NR-Y#VJxbAccy#}U@IAH^y|c_o@!%kyEv#xXUXbtno#%gBAuL@;!MonK}{JWazP zrQsVIiLdcyX~0NXu>kiR0e{68NSS^qW5Hyu!+a-6jZK6`WHJ~@+Z*a=d7damkfPJL z6%j#D+%*ae(9hIZ8wQI%biDQKue{B3$?Mb+;!5xt7uSFG@~Wap>tHP|k7?!pUwV=51RM*Ozb6Ykq} zrJUoEO2N|^e52j_;df#R#TGSFCz6aZ<*!j1Nk&xld*5C+Es+_ILW!lgYra1ot4|ao zIXY?G>JBt+!ArtBIhsTYbB6kwq%F%I9r%OuYN=%q>K4)*V;#%->Jr&hZV94dp3KKL z{#>M(RJk1mEdnTvchBqW0}s}el^KS{vG++>0mIh>h_hjaXg$6FGov`IMk~RZlzCXP zW+C5I?GjSN+WDY}i!V=RdC)D2Of0hz?{oIOWr;7e{PeVIjhWB)7V2IGO1V!q?qR6f zWV@&pVV69Y2jmzT?PA_>-Ue!bHpc(liZ%~Zr>t&DABDWk3@#{K^UCpU_+7GOv@4?L zKzQ_3+?%cn=k6-)D2p;1QQoh{2?_c7>tl(E&JQXQ-$HoqPvTE3R zsb1pOMUWX1OeB9@oRpYj1Uj!)CZ;R8Uu^lQ&R(6^`nx*u{+Ydx9N8K`PFo#iG{dnK zYr7?RbRDaWPdaz=ikQ(xD=AwB?Vb*#1pJZ9}Vk2H{MC% zUI;84hI9vZd^+N79ugu$t*-F-qoqRg^uoRF`-c86j2n$Ce*^N^B6RtFy?lRV!p$Wk zo5u)Z?ww$Y$TfB@d+PM+SAbcBSdH1k};GP zMIM1I!O24|ZKDe0xr0vPL6nBIz={>Us?06_^}Pqo;DAPG#1sGFcR%3qUJ$cQbo;Wo^19SiW$ZFh;l4FOxAlEF(Q` z!0OVFWFzl}wU~Q7NRn^At;B+p+GxgQ^R$+#jYRIwp&w(?uk6M} zgYx@ zj>lqr6IKd_-kX`0)tA?dg^Xu`E%rWG`$fA+!QfMqC!WlmD(vn77UfpQT9|*Y6CSD9 z&4JJkwhgPw3?htFKb88D&v|1Ra8+YIt)b3F_phlU72iKC|2_3*3u?NPRF~OxswcPQwj)eNJkzy zZ>!?%scT@IIFAd|-CZ%AELA?4v(!+3rb1&6nEF?At{rXn#g^JdX}FyybU8`gYMwdo6;BfF7zebeN(l>)ww+jXU$<5IEvKz4up zS#+mc4F)|mD{&S5dk-6!-ri|_TVeBK{5BPVtyWt!7zphu&;2G+07015V6MCiC*QmD z+l3jfWe3nq+S_ZEzdg#>B0d}iqTW+Z54z5JZ{L{w7fY{w`!OLsMet2cTIaU8A7~<_ zq$-8vk0h~OI;~-zkZ8L0?BBe>765vK{pH$EUVH8JS%BJu-TWro0C}Q?NaP3 zyC0rlleVoky#7+~tc3Z7CY;WG{95ns`x}o%;;v6OMlPKZ)-}7V_2uEyxAvb2yFUhf zczy2Ng?B=MA0yA4Ide{*Y8y|0Bln5!o5)aV6%6{(m)I$~7Mp-mQDxv74G55t<;C{G z9C*B-vhNouq%A6?(i3MjR0WXd@N{v}fSCu3;Ih6Gv3)h(LP$?F7eOGJQboZ!kD)wKfqWfu#+oyP2`XB=6FF7O z!F?*u3tuM7s;?i08QtSrA|{G=2B?=MM@!Y;@JD|{$0SB_aCbnIR+5ZT{VOxvwGeaP zWO(oAh=XlHysT5FZVrLRVBu$rT7|>*!)@NRVZO6O0YpPGvXUe$%-5IU1lbn?4EOR= zZ{EBq*i-WXR03Ki46TcyOcRct7$AgndM@Ht7v3tg@u;nSP@{GE=~W+2yR^UqtEHdy z-B{fRLb_gZc?6(=jG(6$N+62*;!BMh@tAc)nwNn<`F^*}~( z+3W>S#b}4i3vmARy#}QHnjDk166OVNhI;?HTTQ1Hs=j;`3~-V016h7dZi`adhc{GR z0L?ady1}YK!XW9w0G3R)fwfE)XfDzzq5k%LdCvkHhn$Tw6!Am*;hxRE&m!SkTKiT@ z_bo{my2C-lnfOlPagoRSkCs=Vwx|!VU#h(+zv|zoFl-|!y-X!_6_6E9pZ+vO{P?-$ zFhBoYdgJt;oD(4a@>TeQkGrC3?ZZ6nOJ~-}!MYC~)J`Ktbu3M*W~=6kX$~Z8Rv_3y z9X6>m5mm+4`kcjOT1UF!bAAWngb%*jfdG{2VWaf)TNYfi>4WaqSfZI_YXY>++eo(6 zC@LDLE8X29+g}WATnNzBeF_}mrb8u`}xo@zJGwzV1)|DGz=Q-KM$kdgVgsGAc7Fj>77;)|I? z&kxGMOWgoDAf<3}awa}~`t*PjJkZ5aOeKio=pK=y$V5c(ux?)uS*}ud?mB8T`#)I# zz(O*XVLQ5Zn12(^Gq<75v+_})IZL|ns{fZ+8BNw1vY{r?ZtA8%&=?FkBIW@3i05$F z&X~9E+A!H!l6&60ey&`~PJA@CRY7*O^t3Ggz{SbDu7Pzo!B4xfUzTDuH{b2z;OJDP za#xKgd3?J;2zB{zIX~!xlVY*yHw!t_JP8l{KH!aY@)47h zo7Kl|(`bfil1LXj!+)#pz+~Z=qt!E7<<9Fq@Gd@7XK#3BRT!l`ao=G9fvmE=X@ts- zD(<{%+n)G)Z6ahG#DkBPSr23FXe#5WC%#w2%#wCvg>TQOdt9d*P<_7rrt$}Aag5#A zNoipPjM7wW2gFeQ;D%fJX-d+%TV>#*L#r)W(OcPFfOCiS9YFmp_Uhzt7@jtTBlGiL zE3Y984c2_^GfWE)U(;j&3*a4|9J66t%`nivlFD27Mdm+>5n;altCDPjt6!}b?-5-< z;V~hOlnuA^LNgI{3k9lz zv)4n!4UVzK{XzpJ4Gj&Gbk@~GJ1t5&%Cu`~T%4S#hoA`X0A6_^!aVaGr0`U0oIZV; zGVe$J5=4wm&=hA1+;1TQpyt5wUj?T$5oZiK6)_=2OD(BH-H?!n7^T8)%_@32GI970 zW!$*wKC+{To{M5*zjO$g5<`VxRzqdB-Ug}hn2&Y?n|WSNlS>Xx`g|V zfB)(1Fa|5}A$wh&J|-2$Qnz!)u9`Nne)v6;vbnw9xMcOcXqVPA;CZyH_QI!3B_y3U zwVJ}@To|?;R>v?Vs<}9i>`efHFE&FZcH){Z1<6uPLwm#IH{<}-%)XT#4~KbWy^?XL zzG%Dd-F%eQ9I;c@ZQwX)iN2STGT$!F=%_5>6_FoUp1k%tJ4|1^F?iW(jY-!W%b19w z=Q14SF2#c{aFY7+z@np@QlpT#;i{Y|?8ASG#$jf{wSazPg5ag>c@q6|TTWi^vRhxn z8tm|qBO(AaNdgRMhZ2+}402GC+uC%MgXdl~us*deVo}s+FKEL98C2{sBlOI$)%d$V z;}?IhG?H0NDldo*$rbkC&D7$-7b9z_6#cvb}DKCrS@AV`@8gz49?>xatGq{uA(s z&rcw-K0l#-vT5x z^VDa((WKQ)2BWRa7VelDuUi}I@7r-AaXyZYX|=;HWSNE{NQ z;7Q!P5@bg`$#CjSG2nG5c8%UQ$I-YJQ->958z_>S$V?xcXMQJ~e!QPn&%W1})x}T3zDG1oHC>bfNhTjU#h3~s1IH2PKSzaK zz*W!3`MKSy$&S@nsl0gm!)*}%?&(or;NW21)nKfasOU$<(a)cECI|s6ZOeI9ML6FW z<{ze>dNUW1Qq$E~259!UkQoGPd6} zXVae%{MOeBb09{21)rqRYBy8@>c5559lT&A@eVfwci7x`Z1Uy`tXIp{)QV7J)C2~o zN!5FlqfDEjZd%ypT6YNcXyV5yKWc}BbVaKXHGm%AeMHjgOGA_fAnem|3w@>^E3%9^pYOFD${i_hb1&tWnYl3Rt~*v*fWB>VP+u!$Bv4qZ?c+9ebqFae_L z6Ur=Xy;%<$%UTu6d*yj4!z?owKM-&oGY0n-VzhpdR3d^drLtrRl%31w4gB32;Hk-N z*j%?RV9jdO6)?5VED`WRZ*ocuG|+;txnDGYQoZb7W)i# zVubV+JextDHMG-ZDkpJ(^m;AVpdSo+73@AX&q~y+m;K{_kF7+Uy*xkqb)4xxh?H7j^80IJ%tkdB`L&9$8 zMHCeVlG~W)hwb_y^-%oam^ic(G(%HBNv{28TUhEBxFM+6N^NT-5_434Q5Hm!(NPfVc= zhnlMJdFq#9Cd47>9uy{ag)`5)Nx+r5lT?e{EVZslA|p{>ca_m51ZivIX(qu)m{;K^ z3sB#V;X8G09&kO#7~6SRjxx7qEWdQGU(x= zM(-@Otl@!I zHBZ0?n!*^%VUXFOB2Od}@R!L#U3>~gz7*qbx*O7tUP~;l-wdEVP9iaZEh2P|f@Qvg zUYe#JCLuz+r=z9>;hmIRG-^q1CPr20Lk|4JeKJ*>6x5$J)sVR9<=gS~Kg>#<-5p?4 znhJuh6c(fChVo$(;^~_Ec_H)KZ;S34dJvlo?7{ygmi}7Qr(=Ns87Uigp?9FXo|A?T z%AJ$t4IlONZ(O^MZ)~FG8jDuD;ZMteY8H! z{i%(Oyb!Iq5+nZ@mmO-Txt=%LifHs^x;<&xtn-eWIiB&V;Cl)d6*e5_Wpx+6-A&YIyD6)h!%q_alHB1$f2XFRE){G^u{>h0Pb!FaN$BU*H{7Hdz zKbw)W@dA$F%4i5C9@DwOevdRmN5(OZ|psxd+A< zz4vkJ= zyMQMnB{XwitAaW`i(nC)ovqiY>@`LWj3N&dDG@f;C+In>soK*XDhj`W0 zlvIZruJn`MSRg?ulSPeTOX_b(tKhy^PnWq|Xj?!p^UKaoDbPdOB8M&eHCY!cXku|e zr1(*089h>Pc;J0KT~8%`DO+!T!&H@zH$*i3GW{@wZwLL&K!06pCKD3=(+=&0fAR(7 zeTbxg7aydj!^LBtfmRE#o0{X96Ax7AY2)7;3QxgJRp;mIJLyxePX}r=Qh7~rZduFe zu6^!#l!BJ$S;OQ!s+{;tW8vgr_g#n{GC~T7O=cTh^h-mqa3js2%%#bv-X3XFSlc^% zqUz3W!?r>C^)JUaHfYF8QF@tjF4^iaG+4?gczP`?J#syL7i82wjrK#XPb5n7V)*t4 zYDlKMJXsSMMsVX__8VPmbl4MaIIvuxIF`jKMmELYYkE;mTZi@3ex$<#)}9{?6bePE zkP``LCIdx*Vy;qKDZ^vaSSZyU(@#V6)ALfai4s=VnPalhb12QIF;%(0brD)phga89 z_dUyIg&bA?l5?m*5!N4Oeb2hs?^n-hRqDn)uXG>Pq9$d;BiH>URh=XS_k;q6YB|jFsyc`3 z35aoO-t~fGR_-oUUnN9DzFB%eQLn6sN_)6vFkgZ?fYDW?6jzy~Um*6Ug_~14#BDx# z->=DnjItU?Gp^F}Jcu#@;9dXnpu0JEIE9G=id81`CEv&-M_0485ltDowr;gTEI&Fm zdL-p^0e?;63RU$SOOjTm5I7`C5B6b1U+ui)_w`i&sa1Bmvz3NApQq7u9?ckvycV6R zOE#CxY?%p$kV6YGE2ai~Ha%7vijX*zZD^#8&su)<$a_U&V{f`yF{zBUNf@SzJGVj`#bchfB<~^)u32OY~1F zdQ@7927z*@EN_(PJ;}A({k!0euCst64#P371P#C-p15pf3(xr;G^--8ZtcvMCNa95 z-}(jI%f1C|flT_Xkv^xn>?*gcI%D@+$h%hiPE~gHGc?ZIr|)q09DmTyv({&zn%0|A zi0jVh#E>$oJ$eu{CH#X%RHfC4 z_3!X$-9Clnh?qC8ZEr7)nKIPBTT0EBy|LBYpYA+v)OX@0#H}vyrFSQ2D65y0$ncWG zl1dRnWS1VXey(P@nb290`3^ZkrB!bpy{1W_y9v#EjaXdPz5cp_LPQiCHS}#?&fX`B zN$Onpp|5(0kamHL$-im7UFqS_3@xH-`i7LvC0pRJJ2U8h zOM!VS7R3@C(EO?pcYvGQht%V24t*M+Id58w@fjaKEK=HfK$hVs?+UdQ-(47I+&uX|sw;(0J zHSLcoP71~-OU0EHPHVuD(wowPvmm06;HXN&0VK4Wpwsdk#*uX``snidSt+%#L8JvE zVqL%RaCcJoaIkI45Gdzhs;9c&QFiX4SI^{XoR|aShs-C7J$oW)tK6bh^jL@DPZhtCO!7Af=$pSI9P! z6LPbVp;14D-Ao&zkB-2d5qj4N;y|lj#BbcRR8CFERz_@l3AyhV{S)baVy z=&dr7-UEn4pu2w>w0nUclq>iRH9^;3qE}k`KP5cL82wphD@{+!THzhicyzXDW2kI6 z{VXJyOI{c;mDXy=s&}6&*EIA@FA%0d`|=`hxxS3%zLF=nuCt_XilPquS?$`df1RZ^ zv{VFd7RYnUQ5Cph;GQp=fz@QG^)hs6{%*JwNk+GGab%k7{F9-B0Q8|)1+;;Qq zn8NV-aYb=uMt7PV@j%-^i+IgtJapsh2E;2Z5mUPRP zUW4!{IbX-Hu$FRNY!J$VBQ`f$e4n|Vf325eR!VP<VmL_nRodGv9Ch+PzTQO{4hx??`>+x=2dV?|Zs}PhIzv(JycV8si*hyT)4iBI_v*7W;19Ir?%Mp=s5*}?1AOgU@tjg}eJl5-6)m+FI;PpCdz0C<4g z9)B3ls{;6C>gKB%S9LyMblrfgy-^U!bIJo5n_Z(Afi$g2$#hSD;Xd2*CTp7QhSk}! z+_&5(RH;p% zAkP<5f-**Cgo?7l+(6M#*KEz;jkRpza3(9G8AfsoT7?pdSr`GNQVaE&D{9iM5` zSsq(5fC$)HWqi(PTJbI9zsqXAY(ZO^xad6%^X$`hxo1c1Ug3k@xxR<>WfSX5D7Jv& z`w7L^^&di^Qvoc*`p8~7vlx1Bn(rXkbuj$r1_)~E-_N3Ar`8JipFz=I2MQ0Y>K7nG z3N)faC);KJ1CU-8ZryQ5eXS`}s1lv0AUud@$U!VN&5fXjmvUz%hnH&yH42>C;v(!~ zk1t;BKcU-Q;I!LFDK^ke48Bx5s-`k?4Cemp2F+kInBQabc)#C#fG|HNjTAzR2OzNyiX>urYd2@=V$aSSN5&+jD zYEo@x2IkG<%`sVfgpAdCTfK;d^hA>`d7YuAXGZT$QPlutehbSS5TSMbwDWBhpl3Ao zLm*tV+(2Hg)k-QZrJp^eA_r;FJ+4Yt*n3=gQNo(>W>_b~D=O+*TU-rCJvG_dYvz0a zWWF}$+$NJpvqPH)Pnydv7#^Ugma3!QkQ4&*enyBLM1Os8y_EhkQ846}SCReo@FDkN zdcFph-1sIEAJ#zdxPEb%b|WL-AXOQaznY#xH%9Yie>@8(>voTdAV3JuRP1+)`FA}= zQ`mrkwLFDIl@n!(X2a5Yb|F8^PzL6BDt2L6L6VfyS9c3(H~3;YBx=om+_vuLb9apH zjC19K{>9T@a$z+TB-0cl95S^&=Gf#IooE3?e5y8DxMxPDhj8#ZaMHuGX&^_GqCEQYl;N+);A@ zm7h%o1B9Buv#~-&$4*{-d;}K40`bN;k;a8NE4@ zMV|#_0rpf&mqr39n@*LGR;Oxs0UHXYZGzP45L-{t!MikvBUU;!@jA3j7s1EN?c$%u z6qc;GyYq5d3{nf`E57+@7W_?o?DdZj7T8^X{pAUEGowO1k3N@j=gPQNjDuI@vs}ez z0h{MK?%81xR_>+UR|DKyGAFU_=BG?Ec*sIiqc5b->d`F>xUO&>yZ9>p;uzx|x)NW2 ze5L5Jw9)se@p8gGfs5A}7&l>(L{SbuW&qqkBbB=I6u#-D!pe+0D-~x3H>%KqzB>b4 zp}iAB`IEP3-%Y@Ck93d!!QJNid=%#cQ=W&Bcl)Y1K_vU19-u&>^xVKtmt4Qv>vl3UB{ZVtd zg=SmdAM-l#zkPFd*1mg&g&(uNJ~%;k?c$qmmuCN<2p>u(o-?HxY@@~7taog^sqiCI zSRB2pd6c9iD=Qx-aBjP=TnzZi`@c!YB|?G0<=6W=?}+s$Im(rse*eCUdEs9}PB3=+ zE*h|vS(0p(4-aC7Yp6-y-TJXw8;ExEy1d$ftWyS!)fK;U)?*g z11y`({rGcde*XonDL}c)dFyg-`3jLcHnnG zT(hVaTUXXUSJz)Z#Yt~nVl7TnBEO&LU5g%UY5}u`{3`wrs%?G*xGS}ljQCZEddvQ< zt~}T#7@>BO%^2{=O>2R90PQP0SL51$4ECdi-FNQW!jT%9jq*Y_dJ>~VT(J2fzwIlA zb54w~t+w!GC(QJV>3(|n=Dgj*PzJpd=@6C(L`Vg-0utMf%eO|Wc~7&BLu`9Ac&WQlV$*V>s($QdU2kP5^#YqFa;o z9!?nk0+M3#!_e%3$rKh~7soPOd1v8N@>$TJL? z$J1`u23{e_SX~IK4f5&Oj4HZ@m@PsmvOw^hHeh#u5T5M&?JtilqJ(d68{6#+NzE~m zPnw!jy8leyO)2z-3BR)2ivvQNb=G&*7LNh~583f?ymhh+6|8sTM%);zb!WN#Z|&JI%MLm>n2ZbG&cFc!AMxhP>LU!OiF_~ zt#2IfLy9yR&z$uQsF47&fU9-=U9m!%H>JQ<5x8(}XNOj!4;=sFTVqBF^IHF0{f##d zeK87nN1z$#Lxy#Hs)xJ$XV3Xx@u;lG!Gt%B<*-5!ieEP#W1Vl(vIt^nMd6jeP13o< zUy&a~WsK<{O;H66i;I4NO*k_P{X&EKj;Du+on+*)pLZ_r!otGgjJ#)l{y+XSDKfd{ zI=CJJ+l^F#B&YqZ#QlNuw>ATWy}LBw5{mQ>T~ z8CVgkLh;6s)l9P3xy_kYZ^Iil7%%HOeNmW8+aFsSu`N;&Wf(c0YMk2Wk=7?Bj4%VH z>7Uq)|Fu3x!TQu{MeOjk>-U2sPUXw4lb}w_buL+qyq=Va$S8Tvy2fiJd)R7J(RowT z6qPOyQG{gWg#3gdTZBR`UA3xl`%z`+Qkk~=l`A8U#HBt)mZw(pRj%xB=9$wJ4k9w+ zk|eFI>_R7ZgQiMu3G-UFEHvYX4t*0O{#Ey9D0#=PFOx@SqNUu%?TC4LXH^>maDjB$ z?I*w7Tv&ML_p>_o6YD2~+c%jU`JOoQzSoFQQ@<0Eas}N>DvV!G?<3a$yZkIRnr2lX zcj9>LiRI+sMGz|W0HZ25`pD8W{vP#?>gcI3dYLWO*>}3 zxYt%t-T8L57K!7V(p+lfFF=SU4b=0E@U^bAihSSX4hqcW%Fgk01@}u88qjgx!*h$j ze*Lj!3 z9*y}L7#H1mlZn?56T5ET)qHz&z;dVKHn(9oYxfS{gC)L`9k-v2R7^e7D5T@Ob=qI$ z*q*%zKa6>$VSre3YVa7ThiI%%bAklI;-%UZw{D0@mXz81Jp5D?H7tqiq|(R?H}HCo z1G`-B6cfPSPJq_TBfWG9ssW68C8RK}hd9Ba*lj=RIEeK@Cfqoa*+H%np*TuWdvl z^K$knDlAj{&<|Ehmq#z9rRf)(uT(VN1W3CRW$dxox6Of}3uIwSB=-Z;7$H^Xouf^$ z8X{TJGxyD8;~tWZodQpRV(Z*g!7 zmP4Y{YlfY_K+-GYKSgr9>dnA)g4@^JUhF49rk8pR7*7Qy&bh9RVLNEH$3Zlt@rQDhim=z#$Vm5}Zp;Qjgh;}O<6 z=i6E5%ge`E!=AnGeeb=m`?{-Bb+~v`GDw!nk3Ku(q+p)e9vBusOq_P+#2f=!{E0ftL;dIxHi~50`g*56A`3wNnhe zbcnF;139L?G;i--8ppKR7XT9g&{`O5z6_+q*wY`Tb$a_HW5}@rsjfxT9lnurNw>Z< zast^vyxOZ)DODiS^Iqm>CNNmy{|LbSmRo%v=GA5=6yjnxSQ8%M4TZD-mcvSgkGVt)-dn+Z!jr6!$*q9xl$&gW0FeqqB)Q#ARH_6(}Q|^I2&K-C27B3?-=$^dWS(BT^n$Na0ieyu*x|3fCjb7|6*2&9VbzmS;$F~FDUc@s+A7E5zFmSX zIFCVU_-%02=^`k)@-NUZTGykhmp}Rc>z|lCc)y`A|%IxDTO5- zAXeWUV-}#ts!>XMaZ!YxCo_U`~2Pc83Dq%LSw=12Cj$O+mAvjt{LMNzJD;2>marn8QD%ykR3#`*!%~Ez;nU^W{C1dZE<|3yKRlfFLY< zFS%f}#=wMThE4cDp4KxqO@3t~+7{(1^v3)06=vSM8q}rZte@_*51sqHtMGll@^R(T{x+#JCTBI321}eKh9$ z+D0Of{Uo~*wpo=G}wtG5&Rr!J8M09?GI8R@xo6{Zr$p(inFP6-$G)t|u+YNCg zZQ5;V#Ua5R@hs!6#?K$=@w2<5ghhy-s7+WOFiP4q_8K*4Pk`Cxiz-SHh8CqEV&=O_ z#LcYykk`!zZA1r6+)C?41J0?C&mY%#-KV*i4y5i4Y7rQRO`7ZkgKfk13pl7011I~^ zJne#tnO+}#+r)ZxDOLFt8Ke`r#} zsL{?nxk^d019mLFKQEPVTR-|0+~n8G(Qg9rCugmfmIHWyRnF(^gGcxaxu8%QJ8zHJ zH&j<6prV?wY3CDnVTARGUGJ#aL20K=)36N5)B3M?39L4t+#}17ATuS z8q#3v;21k)vXv~ulh7U&wLadH6yA1c-RHNOrEfDfb?L5oPuV5%&X`--3%nn7;M8j| zCh$H^^K4%1I3iK%TFfzsT3C&)JA4k-X|D7n(Z9z)MQiPkA#vIH@^Fi{lOKLILH|4o z#dIbN&DG$%W$S;M$?|^2B5tL=38Jh>#bJt)?V_;biale*+~c|x8y%lXwB0MJwCa*7 zxsb#%-H-xqTpy}wBHA2FnDtB&Z5>lVKa%m!a*m1w+mUz(oC>(ciP>y(7_%58#?Llx zBn6v}Fea@vWJz9U2%;d_uO{+j9mdlOScr~XbXJ8l&Y>VGjt&R@ixPC07EG&|11zqf zQ%q-9=^amJQJmIdZ1jDKynQ#PhJ0O#m#zslCSgrhEnfMCTzcKnSK(TbqVTePF z+ze8S<-X^7>T#?^*ZY{7n)SDyhA`PA6c6{*iE`Av@-6Tto$!lF%h!6nBbKW1)rP6{ zm8mosr-E|o(Q>Bei*UJEqZ=v2mk8}=zF_6xZt z`AEG<{?5_%sFr%RS5|TUv|aZ)UWKX=654ul3qd$j92qBZI7ZO6rmV7gnnU_plj5V% zKvk7)i!z|a5t=#s)rv<~&{d~-YaB(=c_^K@2XMtiq@Bj+{qj-0?I1pvafx9)Xp!`k zb66jlxLW^6m90JP-abA`721}Y*ko~9DOX}`+-{2+7Ly zV5o-~^?M50_^W)qP9~qYl2jYI9m6+r=$TcX8cpKCw5v;h4Rd9$tR`p!&6)KeeZ{4o z?P%jxr-515UzLkN`(MRvTgDIPH;T8v2QN;#PucdA=-eujbUrp3+-WY>cTReX0{q++ zaBs-$T*6SH>!v<_t-#jq_njSr+ zKX?Z9;9nf2)9+`gqr-X|NSGvDv_I*q0)5OHFID7H{h6G`wVRV;1u4f`vmtD?C;)O& zccggC!xz8>(Q6gZs-k_pl63xB3#j2VE}z6As({kO4Td|!-%LL^5iT;o#6{_%L<)HKSfF$ry4|2+&-3VUI#CBlf_N~1&cI0noGR$uMD0cAPNcr##8WN zAujhal0RvfxsSfaxU&bTwy`;W{#IIS)>ev!?FAKSeZq-HNOE@S7dn(WMf%n za4281z7l7++sx2!Fo)q`O^*7`fK<0aqE9l>?U`RI;(AWeL>l5zD9-DQ^;*+jRYClM zD#(9BiM#8ogS*q=iSqEqvskJXQZ!cntHKIiG+yIG zn&wHe)$4-l5jsvu@5Vwc)Xq3OVTF zfbaDY*$nY4TH=F^5Hz~Xa_qpQrj})``wA?ul2I$G^c>lMF9m<HP}4j%NwFG zmxOVbUH`~$mr!U|Y@2zs()Q%MM4Tz&`zP}USt$;!ON5F13}K=WXlfFQgXLk)QT6qL zd5$GtB}VU7z2oNd(p9w2XRys}39I6%ssh^YI?n@R&E_SCclC#bb81dx_$Df^Pf$_a zc)7UtLHSI`7H3N_ho_vN6;J&BWRc4Vi+42Jr3?B?mEPt9PJGWzu zO5hP8UlVtl;n*>IM{B}SxfG#qmAj;r+S?;h|Yhv4x26bdoM#|Ud03jT5yQB#X~5zt#>K4-ZYA*}ak z^1S`sc5gUyO{?vV;jF_WPr5p}$1dB$bW@DInch__+EJm%WCOZ=14q$t6cwpzd6H$+ z8a52+e6Thuk&&(R1}&ZAgu8Z72)7x-^u7x77i$UK6r-6kN@5~R@lK6gEYvM1v}u^y zg*5lbBwyoWWBO=ziS=jqT~_;&;gpR1kIoDm4NSik(mWU`McHt5xyI)L1H{ zri8<(t#~^p=(seh7GXGMuELeK_$qT(kJ**SE!8u?@k1Ad>qsT6xZ0-z)z-TGo!7o3 z5S(u}^SLq~Ln|mAmIZG3c)@6NR{~kicaH{DLmLSsuU0)D&iN`A##$H9^)XtFkx--J z7aCU?VkDxYzPfd&u_I}69JoBIL#L4w>y%zXVNUsLE;M1{Ot;(!`pG$+y&OZ% zaOBbZTyMW0X~XNm^J{Mm4$7XNIAU)_Wt2`){`6ke8nSUB<6{t>bjN58?e+fYH}Lhr z`|W{oPPa7__3T?wmKwoI(<~S!u7Z@G^(ZT{k&Wh5ym;$H2v3ENcsD~-=)_WKpKug= znytBHf5`P#Z3(^k9`Mg2KTZ>~8TznpL>uktcE45ObB9S(mO81%-1j#&e!%$$`3|6I z91*PRQam4``|);_u#2a%-wCD}x~bCq`%Zw=u4YZ=2&$!mSufR64WIcM$-}g^e#*fuOYC9+Y9?e#oDaz`{9F0hst!Lwn?Zr}EoC z?8*g<{G;8^juU?ZMPDgz3c!@K(@x&GNTF}%16+Yb^JX&m&s!Pvzf=GJBy|}K?DGjJ zyyts7p70G(E74KagXG41NH!2>as?a)HOEhxBCs>fi58e~pKs#)89HyKBVZ z^L0dC3P*BMt#boVZ?AKOldp1b)nAa)6})(OvDwvTsqajp6b0t^;4{IGL4GpSiSLE8 ze`W3s!2g)o>?`-#sTxeTiRMfbdPs)yzj|mgU!Ue>M(Fi08At>IF^4 z!l_pRG~d;bNAX|h>R$v}xHNEpu6-`Q>||Rf2UOp1EO_Wr4K>rJz&HV~S1&r!K;ezR zv2`5V%c#2ayR?+RI3W7QKi^@1oSXw%fk^SJfBFJ9n_7TXSojo0v)^V5S&oegpDhZT zWbHTNaiq43HHkXNbghmtJ~QB!UT9s9Ly_Lh$b|oxZxv|tip`xC&SvIk(ve`OmI9#k zGbB`}ebFVAc+_RDvl+1R71+ob7V5jjn{9`^g`MW&`E9~Iyw_9W|JjDWq>l3xl#V;A zz=!IVo9RDzS*TI+{+sL1BlSu>h0^(i1l>maG_S)xgTba-BkcFu@%>~!My@F?r)wDTd13ny0->rcrDh6dlY;P>5Y_IVs&5yo#K9@PxpzujX z_c`Y1+`4*fCE?70J zQF5whT~LM#CNMw`-_@m%HV>Gik>rZrl=(Dt9&ue=XozYchDTrLYWxH`Or?0U>gZPX z^GV0mh&axfJ3h+B_w-%pqYJ2l6-w7truH+StkJHCa@A!)}wQQS~4URByeDGoOc@Fjd%@fkweC-G1x|NX95rMvt zNpRuane*c?15`<9l|*TUa&nDcP7-hP=D^qWHm{CfvK3IZk7D*4Zz~+8ZUb-O%lZtM z;q!fxit~YIw1;!q8GVU9iJNJViKgd2eySlfQI;Ev?eOqFLhq87h zeaTHYEmT+}hE8OQ9?(ZH!g(s^x#<}02qrid#>lMIqd;n@&B*mhRP;T_M73aN>f&d! zNhaQ=^kg4fSx*L>>{u~~d%{I(k>0OpLwRK`IcV(5L%7-4v`&4VtfuScifOJu8e@w? zOqH!3f;+L&GPr5Kow5@4kzWvIENCZrpR`FxORH$=)fl`Z6`-f~5#eeU-Jyno*{0?9 zDT+!WUKsb4uM}$CXJP&zJHFH)pqOAjFzDj(B^I{=GKnq9K;x>O>h0)H(y%gepKF50 zWnhyQJcAn;hPH9p5VcxVOI57eIes;X{KB&N&0_IYSSNbV&IkOcW=(|2xgaL;x%s5` zhH|9wx!xV-50B-`?|$dC?x3eV&MvfG|2QOMzdXX``4L7KIw9_|5gaqk6H+;kNY9?b>f*Bn(=P9dUl=ib(yM;o&iM?b0VNrXNh zu-zF`)^1z1zE~w}Nk5hb3yX0@;jy#^i0u7;Tr1T%iR{r_4zUR+ei7`#5{yGtVa{PS zoTpr%bSz%F(ALMH3L^kb$SPC;RI=(;nvp<8lRgLHwo+e{V}?6wrgOgSz1 z_px+=u}}KAvJ88cT@rxwrxVYZ-Csj(c%Rn!L{3w(|4#}6?8uq_Arak+4UR}LjDmkcomHJw2q3CGbG|&0+^kZoIsC4U_ zU+6u9#wPbnMfHX^h3GoZM;E<^epr-%ms5zJe_WkG^mf|a1l0OEpL7C2jH=J9{P1~o z5bXfuY^WGK>&R<~O3&bf$VH;C+)8P68ywzdtj{Ayvi=1@(6$GS-LmR5XtIot${t%;CMm(dldr#Vp5LO_ za_9w;R{FDc=)M(X6SO9F$A(6-cgh7gsW1o2jr)CONuZl47QK$nnNztHUE7CTBfwm` zU3GqYLDg9XhN>2vuojnc{U%nw>Stx-GnBYB)H`UraV;(;Xs4S?d7u)PGKc3ZQhNG+ zIO+$5D!MP=5whwgDUqsp*qbPe#7Cm=oxLlphSch9F|OGagBj;n<(MVIN8MR|HDx?v zVC^wAt`4f+&ve;Fk@Oe;q+1h)Ab$KX)VlFvy7uv#rNtE{5)5H{;{KMo>91@K*Y1O# zXXWSXzwA>DJf3k`@LiyTvHW}~H$z=;vfk6Rfd%^khGxP?C4IC^Ro4~KLRZ-#v7Fm) zV`ELRxEvFV_%S~@wNmX~5ehOK5gP+@20sZZvBXhV3GQuEQ%$dZjZS#A+;I00j@Z;Z03I$ys*F{jO+X>0)&;Cm-|UT)xF zfEqYWp6Sa0Yp2l^;X>S5)_jT-2x}XZ5+|2_nm;Y3g1SyZPCo@$Ll{@C7emJv@2v<+ zJlu3W={1FaZGQMCjSm!9^(Ddsip4#dfIFd=r?$Jx5Hi zMbE?49^Ii5S!Z1n9K=ZE^@ePLs77}vsKDU zmo?;^!ntg~Qv=uDU#hEtV+bX$l*mK{%CTHj%*QubR4ghah7Wa4xLS8U&u=HV&kentrN*=^dM&M;bXb5uW0io)z@+SJX0*4Vpe%N5f9SZ z4Tsi-MO$rmB-nKMho+)+0%TyOgO6%!s-!cb(qiw8?~+~zpLz^ORSB09bm)#+PwtFQ z(C9#|f+@M9a~!_j7QX`kB4$nn9}`wJ82!+vdvKMV^-ZUPT$k;<=K|dcb5s`)cUM(g zd^tt)_AgX*C6JPgXH8OOq2P>F=#Os*(-4pIR9yjoeU9#QJlR#Yn{<~?b>AUnvv?xg zQm08WC|!OQ-_mLRtbb(OgQdT{Ml3EDY}m1@i!8|s@TTHPtVmldukIAh&T5C*KVMP3 z7+U5KB~=2zQ#y?_xo@p%gtRt^V4qjVx8m4nA-8EXWGCW%n1gq&eM@eUpl7@=WBK0& z;`E|>!F#=3$m6h!P#+X7ia#{=Ou#kuUiQW4Kr$(*E57wa7j_4T8GH*2)>7A-%yhwQ z^u@w{aBv|k5ICqUKx0`XOWU!W)}ITj83&DC0p zA5wliZKq_1WADlT_)0hpKJX1WxvjeIO>R4;yUBHoyzFrAKLL9&TnyY@0r+hmLlq?cg_A7>(gh4(Q;5#RV%(tcxZnKK(CD1OC0RrQ zVFmv?Jf6(<5aKoWg^lseZHv1#*7aj*a;|IeUSzymwFD;(-}LBJT81cYPm!bS6X(@5+*LGQF`hnVz5#^uF6v zm2E?n;#V~6zp>%J>5KZOzO#a^hWw`)GNrmMtT?l))GR(1$dp1~uxHM~Sm~Uu!-HmX zkF^TE8(0>(-0pwUg1$5ORsB3{u2)SDs_$VTQx5PiT<3nKcus8`>g$wU>rgqYWYG;` z#S0uH;>v>wN8ShZCiX7IqQB#UDV{$5a}I!N_5uawEvCciFR7G+A8@?5>*OhKTvE*) z;9MjKG7(-H#sH{CpG?e6E_>-w=xPGSSs+1NIualnpq&1qfAHfQ7pPQhJYF8M$Ro}Q zmcYSES49lsv8%aRFjeFeO=6|;uBQhTy~-WTOx^vCE{erhpABW{eeX5>+HzsLq;l{> z*U=oFXNKJN4gKT{<#xaIJzul`?Wz8zrLt7Z*yoAWL5bb2?v#Ev_pmY zbXm!ss-y?UgoXDL)I@$StqIq+D>CbM=z?1icIvtw&t1H(54EMB-U5dq1y5BcNZg*4 z9Aa=G9_J|zpE->&t^FANcrTYK-+XMXgMn}y^=ggyW;}r&*{+edo~FYExD_iV$cC}uTFDmKBJ(U%siQ-O5O775% za4Z;YC^IE1M$1X)u;|xzb`QIbM8sBq5GlO$kX5M64s`Z`HxzC36qg)tjW0QJZ*bzv z$S_Il=bh9M3R*{2x2lH2fF=U3KgpvEoCBUYe8_VCa2tL@~nK^oT z-EN9y&3MCN`)aIaCLDo6@63(2KT?AKE^L#C0~t?CHqe5quO;!<~ndaLw9-3pUZG0Zm3(P;oN^9X<3nahOB zkyD4&u&soA!5BBoEfjsUw;fl+SgJMWXwTzhpi7>xng2}SyEBf$b2RHzNdY^T6*Unk zpskKP8prP&(3@rb?xgrotA0b+mw)ej-(0D3$AU?;b5xGJ)7Pu&<$Fiq2gTF%0iKmV zPp)^3#La|vM6qT^5_yGLPjR1z9wG@4)s4mwW9={o9um7X$UYYn>vhIhk|-9XBkH~^ zDa$-gH@mu`^PfCyTA7K_58(Qz!9?$X`c+CZp3i0Oex8CO>?p7`aFg#h3c}^#p1JYC zt5>@O!Wjk{BTf?ei>SGnON+S=4D`_4o?H`n9X_@Z!iCo-^rm@ybfEsm5sU=2vE*+b zne`!c+;@wC*3M|ZVeB%*80Nf#^*mwLwbbBSd8C+^nWpQRG63Ujsb9lvvlg$d-iAzb z$@^c49sbf&^Y>8-|0tzXo-uU1Bxkl>7G}CW`K4Df!l5rwF{DRC;b^D7(lbTGS$)|M zmR)>@8z`Zb{2@4sv2lh|l!p5TiEm+5qtrgPF`FngXY2HBp;Q z{iDy|p>2eOi~Cy$mnkx7qpPzu7a^?eq?%*u?qiRI-}VyfS-PsBORkKC2_v!^D<0?R zzovWFI68(OkLnZgB@Swr?hZ+ebEaATrMn$YS8 zKX$_bMQZQOa_ac550IChV^PK~vMWJz=)q?k(5cT=O~g8nhLouS9u- zC^nQP4Sx{j<(-c=g2oX8W=A8K5l{oth69V=j-u8r`X=n&*`UR z(10#x0Fn81w!3K%971`9NkIk)EvL#S~xNXznd2fL~I zD<}m|eVo6SoU)ppmgt~sxwHz#IJgd(M`^MDpeF^#jjE#v^kRCD#!9mL^;w2%bZ|)YG;q zrJQ#{aWjgPe;Y-uM4adAth)Psq(eUj1G9iS7ZX3>fr~><-G#0+S+ZSk1loj6aYh2q!{Dhm$ zEIX@Lc~FU_8T*V&m*LFMktRf>5QZ4>1m!-~vJ>G<(qgQTT^Xo)L-g7(Gsb4dL&rOz zQ3+)o7Q^Oa=SVe0(0+F@O=2c#ZZ0@4xJdf4hdovPF2d=D0X;2O zA-;vhbNGu`xXd=-aWv;0QRSKGfLmb2gw7Bqe%{ageqbs$78_oeOYmr(?eXpz7wfB+ zkHxI~s)?$>p3@SpevhNM?q$=BRG(0O*RHVZD$4mtjAvv^O z?QL!dYc%x6+`!BRknV+XNA84S(LHntNd73vyn>>SbcT{5+HqN#Ciop&HWu`TN~G=C zZjv)?eQ&QY9W+X)wG1nobnD>v=+VMgtq z*o>8D>^9~3(+5kkNveM1q$;G0y&j^Z_;Y!XcWn30bk%k`J-<&@O;OYi!O<4MT=I=8 zoPMiBIw~b}#Q!wlY*WK&wKkZ~y=S)i;O&RUN=xtdOgAd1Dqzzx=z6#Oj>NMD5lhXT zz>OHPoxF^@F&v1N7^vxR^iod34rdU!x(#3EQI;Sln&^6J@@;IAgk^JS;iRA6PQBqvv>S?4|e`=HKs0Gz6V-ge|+EHlGk-}tB}F+qo?jl)tj7CW%tB+ zDNZMC@36r67oiW@!EseSW8O(PWFrJ-gBiFJ#4pe&y&&AC5G664C-wS$F_mfqH`k86 z+)HIpGDu|G$yOn)x|MzM104WOh8<$>;QMDq=F_UO$x>g9u*ycZxT{YDIsUpBbP`+n{j{cZfy9oTtR*jq|g1w2Zzl@CX^{f$VB=USlu^J_#oDZu%L0f zy~p?@XgC14i#Hu`(4bxP%WT&($ zyjr9L1PzAhI;AGQ_|y93WqsX1Afzz*Xkpvb4p=jY7;lJrVA+p+O-^Sy?G+n-W(jB0 zj=ZfWYu)H8*4mJg;jGD=nhyvL^&Vce1GsY5UJ+-TiN11T9UOKpT?Lg0}yi zH~^3RR&NLmvkM=!X@#v8v^%REaegcXjcTuzZ>%Bl=GcnW@;6GnQO6Tw7B9r9$?-f* z9t8bE*_g`s!a-&@VTLz;SLrptF1Wp@jFmX?EUJ)XALt-iN@vo)rmnZa5kG38d45oh zO#Qk28KB#c&&8ZSI!zBJqMt~7%s#2=BZD!n!P z3uz>CJr&9*qYo8#+P@l$*W_!|xT=iThR#=-1{5{oRUGP-%M2d(P>%-qk5HQg_#GaU$S+3n}MscH*UK#!bTBwC?|-#*;Es-0TGd-x6I z_$k3vDAiC8W6A2hKuyr*yiGD_R|?!Ope3~A9>g=24x;G)gm25|4#On0@>ieZt+I} zmgWGZh>d}8OAJA)M_+{~K9^Awf4SFNMKp|l`}cKFA={=+VPQ<@fP>Cj8LAY6_$E#? z-R4*VyCg^yAgIM45HCBQeJ#iga;)WVjt3-3tfUT>reg>UXh6kXME7!0|%nel{s>SG@9@{QhkEMnR zp3flb=W9rCpN)j>jEYm+kfkT}$k9ZWWZ*Y^c*4DLO^*JVI)`AG$o?}diFaS_xRc{G z(2Br+e(V3?v`^N3bnH}lB(l@OGh}SM@4fo0+PDyzG3LcTD?Lh=CNV7(errNq{1tfk z9Tpx43;T)`77m#XYnz4d82SMY_LXEVzxupBeVfc;s-Hti&G(IJmcjxOHGzVc1wtV( z!H+&-(Pc4~&Vsj;Tc*c!c&x=S+ZH~a#>A;t&u%&SOH<&G`TjMoHIpya^p&fESEObc zZiFwa@-|JJRt%(EeR*`hdIHZTrcGfMe24yJZO}NlG~{TaIE}BJj<0%^SYPn!9GRu1&N1lRVB z-7R!Je#CO#Uv^$D2VZKQnL7;1QC13|06HoHUWy8?_LCAwJfAzA+P(n zK4~-E$b-QFjQqy6Q!=N#E0_hH=vylSYpFg0=IZ;Z*py z|0DU|^V<}`v$eG)Yby%8ayL$t0+a` zv{S%}@gTcmv(F}tw^^jwd`CX|ADIJtsZT_I{mN`{(PjV^9x|xS;6MWEx`LjGMTbz9 zpS&a6-+DHu7)}!yl($E@Iilr~1ar_QTsnb*(eD2)4hye_d|ZyeZCnIb^7kaq{e1B( zJeAKw@48v@6GEN$OZ20ywj#@Y3hKBPZj$&V%l+W{Tpa7Ob+fWX;6(oirNU}<2t-^q z656xBP(zZc*Nb$u!ouA7TiJZu=E(>47ISlr8}%9NZ~hS2a3JkPfUx{_?#yo6eer$* zKV%S~Gf1Yc?8sLd<*#{}cFbm;lzD&otKy)&K105u+g*ViR<00mP@;Krg6Y9*|W@Ps5fksUu-F`2!>4sllt9?NxLWARFj^+cubsV>4BD}cOVnDgZrw+mxQWRv&e}eH z+;}(t-$h~JVVadT%v{qX*KK1?#c$W+^pO$(drOTMc~|^}a8$Eqa3V;+xe)7iDUqbR z)})LQ-`SyM0atAE=kZa2Th?s9j8;o_xvZvEHM(+OWA*NYlapM?ePK5l;C%i^i~V|7 z(#J}94qO}4W_%8*EP31FPDvJ>$b=Yi_?(a8itFP^Sd$E|R_fgeKG87LJyxoDUskNu z@&2Xr!^ap%N~y3b?aU7$1eRQD$36px`5~qL%|@4*o_+?#i+QYSx*!8v%7VxJatNOS^#>D@B0NCLzZ~y=R literal 0 HcmV?d00001 diff --git a/docs/management/connectors/images/swimlane-params-test.png b/docs/management/connectors/images/swimlane-params-test.png new file mode 100644 index 0000000000000000000000000000000000000000..2dca1850964b3014ab90b2412b391d767b18939c GIT binary patch literal 168149 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�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

}JaKHqb=vop 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 B*@gfB literal 0 HcmV?d00001 diff --git a/docs/management/connectors/index.asciidoc b/docs/management/connectors/index.asciidoc index ea4fa46d3e808..033b1c3ac150e 100644 --- a/docs/management/connectors/index.asciidoc +++ b/docs/management/connectors/index.asciidoc @@ -6,6 +6,7 @@ include::action-types/teams.asciidoc[] include::action-types/pagerduty.asciidoc[] include::action-types/server-log.asciidoc[] include::action-types/servicenow.asciidoc[] +include::action-types/swimlane.asciidoc[] include::action-types/slack.asciidoc[] include::action-types/webhook.asciidoc[] include::pre-configured-connectors.asciidoc[] diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index 3161e97583b72..aa439787ad96f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -25,7 +25,7 @@ import { JiraSecretConfigurationType, JiraExecutorResultData, ExecutorSubActionGetFieldsByIssueTypeParams, - ExecutorSubActionGetIssueTypesParams, + ExecutorSubActionCommonFieldsParams, ExecutorSubActionGetIssuesParams, ExecutorSubActionGetIssueParams, ExecutorSubActionGetIncidentParams, @@ -137,7 +137,7 @@ async function executor( } if (subAction === 'issueTypes') { - const getIssueTypesParams = subActionParams as ExecutorSubActionGetIssueTypesParams; + const getIssueTypesParams = subActionParams as ExecutorSubActionCommonFieldsParams; data = await api.issueTypes({ externalService, params: getIssueTypesParams, From 410036ef2a8e647d19da3c2f24e5b3018d57f07a Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 24 May 2021 10:19:27 +0300 Subject: [PATCH 19/96] Improve type --- x-pack/plugins/cases/common/api/connectors/index.ts | 4 ++-- .../plugins/cases/common/api/connectors/swimlane.ts | 13 +++---------- .../cases/public/components/connectors/index.ts | 4 ++-- .../components/connectors/swimlane/case_fields.tsx | 6 +++--- .../public/components/connectors/swimlane/index.ts | 4 ++-- .../swimlane/external_service_formatter.ts | 6 +++--- 6 files changed, 15 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/cases/common/api/connectors/index.ts b/x-pack/plugins/cases/common/api/connectors/index.ts index 40763d94c3e81..b8c9500330c70 100644 --- a/x-pack/plugins/cases/common/api/connectors/index.ts +++ b/x-pack/plugins/cases/common/api/connectors/index.ts @@ -12,7 +12,7 @@ import { JiraFieldsRT } from './jira'; import { ResilientFieldsRT } from './resilient'; import { ServiceNowITSMFieldsRT } from './servicenow_itsm'; import { ServiceNowSIRFieldsRT } from './servicenow_sir'; -import { SwimlaneUnmappedFieldsRT } from './swimlane'; +import { SwimlaneFieldsRT } from './swimlane'; export * from './jira'; export * from './servicenow_itsm'; @@ -58,7 +58,7 @@ const ConnectorServiceNowITSMTypeFieldsRt = rt.type({ const ConnectorSwimlaneTypeFieldsRt = rt.type({ type: rt.literal(ConnectorTypes.swimlane), - fields: rt.union([SwimlaneUnmappedFieldsRT, rt.null]), + fields: rt.union([SwimlaneFieldsRT, rt.null]), }); const ConnectorServiceNowSIRTypeFieldsRt = rt.type({ diff --git a/x-pack/plugins/cases/common/api/connectors/swimlane.ts b/x-pack/plugins/cases/common/api/connectors/swimlane.ts index bdd84b221b3cf..2f36a5d554d92 100644 --- a/x-pack/plugins/cases/common/api/connectors/swimlane.ts +++ b/x-pack/plugins/cases/common/api/connectors/swimlane.ts @@ -6,20 +6,13 @@ */ import * as rt from 'io-ts'; -export const SwimlaneUnmappedFieldsRT = rt.type({ + +// New fields should also be added at: x-pack/plugins/cases/server/connectors/case/schema.ts +export const SwimlaneFieldsRT = rt.type({ alertSource: rt.union([rt.string, rt.null]), caseId: rt.union([rt.string, rt.null]), caseName: rt.union([rt.string, rt.null]), severity: rt.union([rt.string, rt.null]), }); -// New fields should also be added at: x-pack/plugins/cases/server/connectors/case/schema.ts -export const SwimlaneFieldsRT = rt.intersection([ - SwimlaneUnmappedFieldsRT, - rt.type({ - alertName: rt.string, - comments: rt.union([rt.string, rt.null]), - }), -]); export type SwimlaneFieldsType = rt.TypeOf; -export type SwimlaneUnmappedFieldsType = rt.TypeOf; diff --git a/x-pack/plugins/cases/public/components/connectors/index.ts b/x-pack/plugins/cases/public/components/connectors/index.ts index 78c2e4ad0d208..d8b028e977bca 100644 --- a/x-pack/plugins/cases/public/components/connectors/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/index.ts @@ -16,7 +16,7 @@ import { ServiceNowITSMFieldsType, ServiceNowSIRFieldsType, ResilientFieldsType, - SwimlaneUnmappedFieldsType, + SwimlaneFieldsType, } from '../../../common'; export { getActionType as getCaseConnectorUi } from './case'; @@ -43,7 +43,7 @@ class CaseConnectors { getServiceNowITSMCaseConnector() ); this.caseConnectorsRegistry.register(getServiceNowSIRCaseConnector()); - this.caseConnectorsRegistry.register(getSwimlaneCaseConnector()); + this.caseConnectorsRegistry.register(getSwimlaneCaseConnector()); } registry(): CaseConnectorsRegistry { diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx index 676d16a767bbf..79c08c3be16c6 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx @@ -9,11 +9,11 @@ import React, { useMemo, useCallback } from 'react'; import { EuiFieldText, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { ConnectorFieldsProps } from '../types'; -import { ConnectorTypes, SwimlaneUnmappedFieldsType } from '../../../../common'; +import { ConnectorTypes, SwimlaneFieldsType } from '../../../../common'; import { ConnectorCard } from '../card'; import { fieldLabels } from './index'; const SwimlaneFieldsComponent: React.FunctionComponent< - ConnectorFieldsProps + ConnectorFieldsProps > = ({ isEdit = true, fields, connector, onChange }) => { const { alertSource, caseId, caseName, severity } = fields || { alertSource: null, @@ -39,7 +39,7 @@ const SwimlaneFieldsComponent: React.FunctionComponent< () => Object.entries({ alertSource, caseId, caseName, severity }).reduce( (acc: Array<{ title: string; description: string }>, [key, value]) => { - const fieldName = key as keyof SwimlaneUnmappedFieldsType; + const fieldName = key as keyof SwimlaneFieldsType; return [ ...acc, ...(value !== null && value !== '' diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts index c694cef3e2562..0503266ba3523 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts @@ -8,10 +8,10 @@ import { lazy } from 'react'; import { CaseConnector } from '../types'; -import { ConnectorTypes, SwimlaneUnmappedFieldsType } from '../../../../common'; +import { ConnectorTypes, SwimlaneFieldsType } from '../../../../common'; import * as i18n from './translations'; -export const getCaseConnector = (): CaseConnector => { +export const getCaseConnector = (): CaseConnector => { return { id: ConnectorTypes.swimlane, fieldsComponent: lazy(() => import('./case_fields')), diff --git a/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts b/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts index fd0b67be72e2c..d0f49cef9f17f 100644 --- a/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts +++ b/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts @@ -6,14 +6,14 @@ */ import { ExternalServiceFormatter } from '../types'; -import { ConnectorSwimlaneTypeFields, SwimlaneUnmappedFieldsType } from '../../../common'; +import { ConnectorSwimlaneTypeFields, SwimlaneFieldsType } from '../../../common'; -const format: ExternalServiceFormatter['format'] = (theCase) => { +const format: ExternalServiceFormatter['format'] = (theCase) => { const { alertSource = null, caseId = null, caseName = null, severity = null } = (theCase.connector.fields as ConnectorSwimlaneTypeFields['fields']) ?? {}; return { alertSource, caseId, caseName, severity }; }; -export const swimlaneExternalServiceFormatter: ExternalServiceFormatter = { +export const swimlaneExternalServiceFormatter: ExternalServiceFormatter = { format, }; From 060531a34bb82a1e5f2e5f9b8fd12dc7108c1e19 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 24 May 2021 10:22:05 +0300 Subject: [PATCH 20/96] Fix case connector schema --- x-pack/plugins/cases/server/connectors/case/schema.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/cases/server/connectors/case/schema.ts b/x-pack/plugins/cases/server/connectors/case/schema.ts index e016ce6577eee..ce1202cade335 100644 --- a/x-pack/plugins/cases/server/connectors/case/schema.ts +++ b/x-pack/plugins/cases/server/connectors/case/schema.ts @@ -52,11 +52,9 @@ const JiraFieldsSchema = schema.object({ }); const SwimlaneFieldsSchema = schema.object({ - alertName: schema.string(), alertSource: schema.nullable(schema.string()), caseId: schema.nullable(schema.string()), caseName: schema.nullable(schema.string()), - comments: schema.nullable(schema.string()), severity: schema.nullable(schema.string()), }); From dcaab5b0f5f0c9a5ce7853dd8c1c1f6356947a7c Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 24 May 2021 11:48:15 +0300 Subject: [PATCH 21/96] Move getApplicatio to the backend --- .../builtin_action_types/swimlane/api.ts | 9 +++ .../builtin_action_types/swimlane/index.ts | 8 +- .../builtin_action_types/swimlane/schema.ts | 5 ++ .../builtin_action_types/swimlane/service.ts | 26 +++++++ .../builtin_action_types/swimlane/types.ts | 8 +- .../connectors/swimlane/case_fields.tsx | 7 +- .../builtin_action_types/swimlane/api.ts | 42 ++++------ .../swimlane/steps/swimlane_connection.tsx | 29 ++++--- .../swimlane/swimlane_connectors.tsx | 2 +- .../swimlane/translations.ts | 14 ++-- .../swimlane/use_get_application.tsx | 76 +++++++++++++++++++ 11 files changed, 173 insertions(+), 53 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts index ec6850bf1e786..18d4bc286d094 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts @@ -11,8 +11,16 @@ import { ExternalServiceApi, Incident, PushToServiceApiHandlerArgs, + GetApplicationResponse, + GetApplicationHandlerArgs, } from './types'; +const getApplicationHandler = async ({ + externalService, +}: GetApplicationHandlerArgs): Promise => { + return await externalService.getApplication(); +}; + const createRecordHandler = async ({ externalService, params, @@ -52,6 +60,7 @@ const pushToServiceHandler = async ({ }; export const api: ExternalServiceApi = { + getApplication: getApplicationHandler, createRecord: createRecordHandler, pushToService: pushToServiceHandler, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts index 5abb7b2e5c5d6..6de2dd257ecac 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts @@ -33,7 +33,7 @@ interface GetActionTypeParams { configurationUtilities: ActionsConfigurationUtilities; } -const supportedSubActions: string[] = ['application', 'createRecord', 'pushToService']; +const supportedSubActions: string[] = ['getApplication', 'createRecord', 'pushToService']; // action type definition export function getActionType( @@ -101,6 +101,12 @@ async function executor( throw new Error(errorMessage); } + if (subAction === 'getApplication') { + data = await api.getApplication({ + externalService, + }); + } + if (subAction === 'createRecord') { const createRecordParams = subActionParams as ExecutorSubActionCreateRecordParams; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts index 86c832832ae5f..59df49e63cc5f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -50,6 +50,7 @@ const SwimlaneFields = { severity: schema.nullable(schema.string()), }; +export const ExecutorSubActionGetApplicationParamsSchema = schema.object({}); export const ExecutorSubActionCreateRecordParamsSchema = schema.object(SwimlaneFields); export const ExecutorSubActionPushParamsSchema = schema.object({ @@ -68,6 +69,10 @@ export const ExecutorSubActionPushParamsSchema = schema.object({ }); export const ExecutorParamsSchema = schema.oneOf([ + schema.object({ + subAction: schema.literal('getApplication'), + subActionParams: ExecutorSubActionGetApplicationParamsSchema, + }), schema.object({ subAction: schema.literal('createRecord'), subActionParams: ExecutorSubActionCreateRecordParamsSchema, diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index 1f4bdd440f258..cbe64f376fa54 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -17,6 +17,7 @@ import { ExternalService, ExternalServiceCredentials, ExternalServiceIncidentResponse, + GetApplicationResponse, MappingConfigType, SwimlaneComment, SwimlanePublicConfigurationType, @@ -51,6 +52,8 @@ export const createExternalService = ( ? urlWithoutTrailingSlash : urlWithoutTrailingSlash + '/api'; + const applicationUrl = `${apiUrl}/app/${appId}`; + const getPostRecordUrl = (id: string) => `${apiUrl}/app/${id}/record`; const getPostRecordIdUrl = (id: string, recordId: string) => @@ -65,6 +68,28 @@ export const createExternalService = ( const getCommentFieldId = (fieldMappings: MappingConfigType): string | null => fieldMappings.commentsConfig?.id || null; + const getApplication = async (): Promise => { + try { + const res = await request({ + axios: axiosInstance, + configurationUtilities, + headers, + logger, + method: 'get', + url: applicationUrl, + }); + + return res.data; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to get application with id ${appId}. Error: ${error.message}` + ) + ); + } + }; + const createRecord = async ( params: CreateRecordParams ): Promise => { @@ -209,5 +234,6 @@ export const createExternalService = ( createComment, createRecord, updateRecord, + getApplication, }; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts index 92cd121dc4717..3ef384a660fa4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts @@ -70,6 +70,10 @@ export interface FieldConfig { fieldType: string; } +export interface GetApplicationResponse { + fields: FieldConfig[]; +} + export interface SwimlaneRecordPayload { applicationId: string; id?: string; @@ -78,6 +82,7 @@ export interface SwimlaneRecordPayload { } export interface ExternalService { + getApplication: () => Promise; createComment: (params: CreateCommentParams) => Promise; createRecord: (params: CreateRecordParams) => Promise; updateRecord: (params: UpdateRecordParams) => Promise; @@ -101,11 +106,12 @@ export interface GetApplicationHandlerArgs { } export interface ExternalServiceApi { + getApplication: (args: GetApplicationHandlerArgs) => Promise; createRecord: (args: CreateRecordApiHandlerArgs) => Promise; pushToService: (args: PushToServiceApiHandlerArgs) => Promise; } -export type SwimlaneExecutorResultData = ExternalServiceIncidentResponse; +export type SwimlaneExecutorResultData = ExternalServiceIncidentResponse | GetApplicationResponse; export type SwimlaneDataValues = Record; export interface SwimlaneComment { fieldId: string; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx index 79c08c3be16c6..d896e21f89d2f 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx @@ -15,12 +15,7 @@ import { fieldLabels } from './index'; const SwimlaneFieldsComponent: React.FunctionComponent< ConnectorFieldsProps > = ({ isEdit = true, fields, connector, onChange }) => { - const { alertSource, caseId, caseName, severity } = fields || { - alertSource: null, - caseId: null, - caseName: null, - severity: null, - }; + const { alertSource = null, caseId = null, caseName = null, severity = null } = fields ?? {}; const onFieldChange = useCallback( (key, value) => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts index c0e8c1d9d00d9..a84a9df039896 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts @@ -6,35 +6,25 @@ */ import { HttpSetup } from 'kibana/public'; +import { BASE_ACTION_API_PATH } from '../../../constants'; +import { SwimlaneFieldMappingConfig } from './types'; export async function getApplication({ http, - url, - appId, - apiToken, + signal, + connectorId, }: { http: HttpSetup; - url: string; - appId: string; - apiToken: string; -}): Promise> { - const headers: Record = { - 'Content-Type': 'application/json', - 'Private-Token': `${apiToken}`, - }; - - const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url; - const apiUrl = urlWithoutTrailingSlash.endsWith('api') - ? urlWithoutTrailingSlash - : urlWithoutTrailingSlash + '/api'; - const applicationUrl = `${apiUrl}/app/{appId}`; - - const getApplicationUrl = (id: string) => applicationUrl.replace('{appId}', id); - try { - return await http.get(getApplicationUrl(appId), { - headers, - }); - } catch (error) { - throw new Error(`Unable to get application with id ${appId}. Error: ${error.message}`); - } + signal: AbortSignal; + connectorId: string; +}): Promise<{ fields: SwimlaneFieldMappingConfig[] }> { + return await http.post( + `${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`, + { + body: JSON.stringify({ + params: { subAction: 'getApplication', subActionParams: {} }, + }), + signal, + } + ); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx index a3db226708b06..a35af7ce111a6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx @@ -18,7 +18,7 @@ import { FormattedMessage } from 'react-intl'; import * as i18n from '../translations'; import { StepProps } from './'; import { useKibana } from '../../../../../common/lib/kibana'; -import { getApplication } from '../api'; +import { useGetApplication } from '../use_get_application'; export const SwimlaneConnection: React.FunctionComponent = ({ action, @@ -29,22 +29,31 @@ export const SwimlaneConnection: React.FunctionComponent = ({ updateCurrentStep, updateFields, }) => { - const { http } = useKibana().services; + const { + http, + notifications: { toasts }, + } = useKibana().services; const { apiUrl, appId } = action.config; const { apiToken } = action.secrets; const { docLinks } = useKibana().services; + const { getApplication } = useGetApplication({ + http, + toastNotifications: toasts, + action, + }); const isValid = useMemo(() => apiUrl && apiToken && appId, [apiToken, apiUrl, appId]); + const connectSwimlane = useCallback(async () => { // fetch swimlane application configuration - const application = await getApplication({ http, url: apiUrl, appId, apiToken }); + const application = await getApplication(); - if (!application) { - throw new Error(i18n.SW_GET_APPLICATION_API_ERROR(appId)); + if (application != null) { + const allFields = application.fields; + updateFields(allFields); + updateCurrentStep(2); } - const allFields = application.fields; - updateFields(allFields); - updateCurrentStep(2); - }, [apiToken, apiUrl, appId, http, updateCurrentStep, updateFields]); + }, [getApplication, updateCurrentStep, updateFields]); + const onChangeConfig = useCallback( (e: React.ChangeEvent, key: 'apiUrl' | 'appId') => { editActionConfig(key, e.target.value); @@ -142,7 +151,7 @@ export const SwimlaneConnection: React.FunctionComponent = ({ fullWidth isInvalid={isInvalid} readOnly={readOnly} - value={apiToken || ''} + value={apiToken ?? ''} data-test-subj="swimlaneApiTokenInput" onChange={onChangeSecrets} onBlur={onBlurSecrets} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx index 6bf2bae37765c..01cae4c7de96b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx @@ -12,7 +12,7 @@ import { ActionConnectorFieldsProps } from '../../../../types'; import { SwimlaneActionConnector, SwimlaneFieldMappingConfig } from './types'; import { SwimlaneConnection, SwimlaneFields } from './steps'; -const SwimlaneActionConnectorFields: React.FunctionComponent< +const SwimlaneActionConnectorFields: React.FC< ActionConnectorFieldsProps > = ({ errors, action, editActionConfig, editActionSecrets, readOnly }) => { const [currentStep, setCurrentStep] = useState(1); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts index 61dee9e031ccd..74dd783e1d191 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts @@ -49,14 +49,12 @@ export const SW_REQUIRED_API_TOKEN_TEXT = i18n.translate( } ); -export const SW_GET_APPLICATION_API_ERROR = (id: string | null) => - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.swimlane.unableToGetApplicationMessage', - { - defaultMessage: 'Unable to get application with id {id}', - values: { id }, - } - ); +export const SW_GET_APPLICATION_API_ERROR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlane.unableToGetApplicationMessage', + { + defaultMessage: 'Unable to get application', + } +); export const SW_API_URL_TEXT_FIELD_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiUrlTextFieldLabel', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx new file mode 100644 index 0000000000000..c97098691b9b4 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx @@ -0,0 +1,76 @@ +/* + * 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 { useState, useCallback, useRef } from 'react'; +import { HttpSetup, ToastsApi } from 'kibana/public'; +import { getApplication as getApplicationApi } from './api'; +import * as i18n from './translations'; +import { SwimlaneActionConnector, SwimlaneFieldMappingConfig } from './types'; + +interface Props { + http: HttpSetup; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + action: SwimlaneActionConnector; +} + +export interface UseGetApplication { + getApplication: () => Promise<{ fields: SwimlaneFieldMappingConfig[] } | undefined>; + isLoading: boolean; +} + +export const useGetApplication = ({ + http, + action, + toastNotifications, +}: Props): UseGetApplication => { + const [isLoading, setIsLoading] = useState(false); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); + + const getApplication = useCallback(async () => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + setIsLoading(true); + + const response = await getApplicationApi({ + http, + signal: abortCtrlRef.current.signal, + connectorId: action.id, + }); + + setIsLoading(false); + + if (!isCancelledRef.current) { + toastNotifications.addDanger({ + title: i18n.SW_GET_APPLICATION_API_ERROR, + }); + } + + return response; + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.SW_GET_APPLICATION_API_ERROR, + text: error.message, + }); + } + setIsLoading(false); + } + } + }, [action, http, toastNotifications]); + + return { + isLoading, + getApplication, + }; +}; From 439ecff74dbf5c515c50af019f86042532785b3a Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 24 May 2021 12:10:32 +0300 Subject: [PATCH 22/96] Prevent prototype polution from fields --- .../swimlane/helpers.test.ts | 47 +++++++++++++++++-- .../builtin_action_types/swimlane/helpers.ts | 10 ++++ .../builtin_action_types/swimlane/service.ts | 8 +++- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts index 46e950de8679b..47a338af06802 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts @@ -6,7 +6,7 @@ */ import { MappingConfigType } from './types'; -import { getBodyForEventAction } from './helpers'; +import { getBodyForEventAction, removeUnsafeFields } from './helpers'; describe('Create Record Mapping', () => { let mappingConfig: MappingConfigType; @@ -66,11 +66,48 @@ describe('Create Record Mapping', () => { const data = getBodyForEventAction(appId, mappingConfig, params); expect(data?.values?.[mappingConfig.alertSourceConfig?.id ?? 0]).toEqual(params.alertSource); expect(data?.values?.[mappingConfig.alertNameConfig.id]).toEqual(params.alertName); - // @ts-ignore - expect(data?.values?.[mappingConfig.caseNameConfig.id]).toEqual(params.caseName); + expect(data?.values?.[mappingConfig.caseNameConfig?.id ?? 0]).toEqual(params.caseName); expect(data?.values?.[mappingConfig.caseIdConfig?.id ?? 0]).toEqual(params.caseId); - // @ts-ignore - expect(data?.values?.[mappingConfig.commentsConfig.id]).toEqual(params.comments); + expect(data?.values?.[mappingConfig.commentsConfig?.id ?? 0]).toEqual(params.comments); expect(data?.values?.[mappingConfig?.severityConfig?.id ?? 0]).toEqual(params.severity); }); }); + +describe('removeUnsafeFields', () => { + const fields = [ + { + id: '__proto__', + name: 'Alert Source', + key: 'alert-source', + fieldType: 'text', + }, + { + id: 'adnjls', + name: '__proto__', + key: 'alert-source', + fieldType: 'text', + }, + { + id: 'adnjls', + name: 'Alert Source', + key: '__proto__', + fieldType: 'text', + }, + { + id: 'adnjls', + name: 'Alert Source', + key: 'alert-source', + fieldType: '__proto__', + }, + { + id: 'safe', + name: 'safe', + key: 'safe', + fieldType: 'safe', + }, + ]; + test('it returns only safe fields', () => { + const safeFields = removeUnsafeFields(fields); + expect(safeFields).toEqual([fields[4]]); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts index daa34bb731e6b..aaa33af3bf534 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts @@ -8,6 +8,7 @@ import { CreateRecordParams, ExecutorSubActionCreateRecordParams, + FieldConfig, MappingConfigType, SwimlaneDataComments, SwimlaneDataValues, @@ -86,3 +87,12 @@ export const removeCommentFieldUpdatedInformation = (content: string): string => } return content; }; + +export const removeUnsafeFields = (fields: FieldConfig[]): FieldConfig[] => + fields.filter( + (filter) => + filter.id !== '__proto__' && + filter.key !== '__proto__' && + filter.name !== '__proto__' && + filter.fieldType !== '__proto__' + ); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index cbe64f376fa54..cd1ff8d760441 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -10,7 +10,11 @@ import axios from 'axios'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { getErrorMessage, request } from '../lib/axios_utils'; -import { getBodyForEventAction, removeCommentFieldUpdatedInformation } from './helpers'; +import { + getBodyForEventAction, + removeCommentFieldUpdatedInformation, + removeUnsafeFields, +} from './helpers'; import { CreateCommentParams, CreateRecordParams, @@ -79,7 +83,7 @@ export const createExternalService = ( url: applicationUrl, }); - return res.data; + return { ...res.data, fields: removeUnsafeFields(res.data?.fields ?? []) }; } catch (error) { throw new Error( getErrorMessage( From a4f27d1bf38466df5f76e0a0d658f7e58ac70369 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 24 May 2021 14:55:41 +0300 Subject: [PATCH 23/96] Add tests --- .../builtin_action_types/swimlane/api.test.ts | 19 +- .../swimlane/helpers.test.ts | 58 +----- .../builtin_action_types/swimlane/mocks.ts | 79 ++++---- .../swimlane/service.test.ts | 157 +++++++++++++++- .../builtin_action_types/swimlane/service.ts | 2 +- .../builtin_action_types/swimlane/api.test.ts | 39 ++++ .../builtin_action_types/swimlane/api.ts | 6 +- .../swimlane/use_get_application.test.tsx | 176 ++++++++++++++++++ .../swimlane/use_get_application.tsx | 16 +- 9 files changed, 458 insertions(+), 94 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts index b36e2c1a06849..3badb94e6d4d4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts @@ -7,8 +7,14 @@ import { api } from './api'; import { ExternalService } from './types'; -import { externalServiceMock, recordResponseCreate, recordResponseUpdate } from './mocks'; +import { + externalServiceMock, + recordResponseCreate, + recordResponseUpdate, + getApplicationResponse, +} from './mocks'; import { Logger } from '@kbn/logging'; + let mockedLogger: jest.Mocked; const params = { alertName: 'alert name', @@ -18,6 +24,7 @@ const params = { caseId: '123456', comments: 'some comments', }; + describe('api', () => { let externalService: jest.Mocked; @@ -25,6 +32,13 @@ describe('api', () => { externalService = externalServiceMock.create(); }); + describe('getApplication', () => { + test('it returns the fields correctly', async () => { + const res = await api.getApplication({ externalService }); + expect(res).toEqual(getApplicationResponse); + }); + }); + describe('createRecord', () => { test('it creates a record correctly with a comment', async () => { const res = await api.createRecord({ @@ -61,6 +75,7 @@ describe('api', () => { expect(externalService.updateRecord).not.toHaveBeenCalled(); expect(res).toEqual(recordResponseCreate); }); + test('it pushes a new record with a comment', async () => { await api.pushToService({ externalService, @@ -75,6 +90,7 @@ describe('api', () => { }); expect(externalService.createComment).toHaveBeenCalled(); }); + test('updates existing record', async () => { const res = await api.pushToService({ externalService, @@ -87,6 +103,7 @@ describe('api', () => { comments: [{ comment: 'some comments', commentId: '123' }], }, }); + expect(externalService.createComment).toHaveBeenCalled(); expect(externalService.createRecord).not.toHaveBeenCalled(); expect(externalService.updateRecord).toHaveBeenCalled(); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts index 47a338af06802..aed3e406e4e9a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts @@ -5,54 +5,12 @@ * 2.0. */ -import { MappingConfigType } from './types'; import { getBodyForEventAction, removeUnsafeFields } from './helpers'; +import { mappings } from './mocks'; describe('Create Record Mapping', () => { - let mappingConfig: MappingConfigType; const appId = '45678'; - beforeAll(() => { - mappingConfig = { - alertSourceConfig: { - id: 'adnjls', - name: 'Alert Source', - key: 'alert-source', - fieldType: 'text', - }, - severityConfig: { - id: 'adnlas', - name: 'Severity', - key: 'severity', - fieldType: 'text', - }, - alertNameConfig: { - id: 'adnfls', - name: 'Alert Name', - key: 'alert-name', - fieldType: 'text', - }, - caseIdConfig: { - id: 'a6sst', - name: 'Case Id', - key: 'case-id-name', - fieldType: 'text', - }, - caseNameConfig: { - id: 'a6fst', - name: 'Case Name', - key: 'case-name', - fieldType: 'text', - }, - commentsConfig: { - id: 'a6fdf', - name: 'Comments', - key: 'comments', - fieldType: 'text', - }, - }; - }); - test('Mapping is Successful', () => { const params = { alertName: 'Alert Name', @@ -63,13 +21,13 @@ describe('Create Record Mapping', () => { comments: 'This is a comment', externalId: null, }; - const data = getBodyForEventAction(appId, mappingConfig, params); - expect(data?.values?.[mappingConfig.alertSourceConfig?.id ?? 0]).toEqual(params.alertSource); - expect(data?.values?.[mappingConfig.alertNameConfig.id]).toEqual(params.alertName); - expect(data?.values?.[mappingConfig.caseNameConfig?.id ?? 0]).toEqual(params.caseName); - expect(data?.values?.[mappingConfig.caseIdConfig?.id ?? 0]).toEqual(params.caseId); - expect(data?.values?.[mappingConfig.commentsConfig?.id ?? 0]).toEqual(params.comments); - expect(data?.values?.[mappingConfig?.severityConfig?.id ?? 0]).toEqual(params.severity); + const data = getBodyForEventAction(appId, mappings, params); + expect(data?.values?.[mappings.alertSourceConfig?.id ?? 0]).toEqual(params.alertSource); + expect(data?.values?.[mappings.alertNameConfig.id]).toEqual(params.alertName); + expect(data?.values?.[mappings.caseNameConfig?.id ?? 0]).toEqual(params.caseName); + expect(data?.values?.[mappings.caseIdConfig?.id ?? 0]).toEqual(params.caseId); + expect(data?.values?.[mappings.commentsConfig?.id ?? 0]).toEqual(params.comments); + expect(data?.values?.[mappings?.severityConfig?.id ?? 0]).toEqual(params.severity); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts index 81e8ac4f2302a..7e4f71485a2ea 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts @@ -11,68 +11,83 @@ import { ExternalService, } from './types'; -export const recordResponseCreate = { - id: '123456', - title: 'neato', - url: 'swimlane.com', -}; -export const recordResponseUpdate = { - id: '98765', - title: 'not neato', - url: 'laneswim.com', -}; -export const commentResponse = { - id: '123456', -}; -const createMock = (): jest.Mocked => { - return { - createComment: jest.fn().mockImplementation(() => Promise.resolve(commentResponse)), - createRecord: jest.fn().mockImplementation(() => Promise.resolve(recordResponseCreate)), - updateRecord: jest.fn().mockImplementation(() => Promise.resolve(recordResponseUpdate)), - }; -}; - -const externalServiceMock = { - create: createMock, -}; - -export const mappings = { - alertSourceConfig: { +export const applicationFields = [ + { id: 'adnjls', name: 'Alert Source', key: 'alert-source', fieldType: 'text', }, - severityConfig: { + { id: 'adnlas', name: 'Severity', key: 'severity', fieldType: 'text', }, - alertNameConfig: { + { id: 'adnfls', name: 'Alert Name', key: 'alert-name', fieldType: 'text', }, - caseIdConfig: { + { id: 'a6sst', name: 'Case Id', key: 'case-id-name', fieldType: 'text', }, - caseNameConfig: { + { id: 'a6fst', name: 'Case Name', key: 'case-name', fieldType: 'text', }, - commentsConfig: { + { id: 'a6fdf', name: 'Comments', key: 'comments', fieldType: 'text', }, +]; + +export const mappings = { + alertSourceConfig: applicationFields[0], + severityConfig: applicationFields[1], + alertNameConfig: applicationFields[2], + caseIdConfig: applicationFields[3], + caseNameConfig: applicationFields[4], + commentsConfig: applicationFields[5], +}; + +export const getApplicationResponse = { fields: applicationFields }; + +export const recordResponseCreate = { + id: '123456', + title: 'neato', + url: 'swimlane.com', +}; + +export const recordResponseUpdate = { + id: '98765', + title: 'not neato', + url: 'laneswim.com', +}; + +export const commentResponse = { + id: '123456', +}; + +const createMock = (): jest.Mocked => { + return { + getApplication: jest.fn().mockImplementation(() => Promise.resolve(getApplicationResponse)), + createComment: jest.fn().mockImplementation(() => Promise.resolve(commentResponse)), + createRecord: jest.fn().mockImplementation(() => Promise.resolve(recordResponseCreate)), + updateRecord: jest.fn().mockImplementation(() => Promise.resolve(recordResponseUpdate)), + }; +}; + +const externalServiceMock = { + create: createMock, }; const executorParams: ExecutorSubActionCreateRecordParams = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts index 5f16e8eeb1c64..34c28179b5b9b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts @@ -10,8 +10,10 @@ import axios from 'axios'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { Logger } from '../../../../../../src/core/server'; import { actionsConfigMock } from '../../actions_config.mock'; +import * as utils from '../lib/axios_utils'; import { createExternalService } from './service'; -import { mappings } from './mocks'; +import { mappings, applicationFields } from './mocks'; +import { ExternalService } from './types'; const logger = loggingSystemMock.create().get() as jest.Mocked; @@ -25,9 +27,45 @@ jest.mock('../lib/axios_utils', () => { }); axios.create = jest.fn(() => axios); +const requestMock = utils.request as jest.Mock; const configurationUtilities = actionsConfigMock.create(); describe('Swimlane Service', () => { + let service: ExternalService; + const config = { + apiUrl: 'https://test.swimlane.com/', + appId: 'bcq16kdTbz5jlwM6h', + mappings, + }; + + const headers = { + 'Content-Type': 'application/json', + 'Private-Token': 'token', + 'kbn-xsrf': 'true', + }; + + const incident = { + alertName: 'Alert Name', + alertSource: 'Alert Source', + caseId: 'Case Id', + caseName: 'Case Name', + comments: 'Comments', + severity: 'Severity', + externalId: null, + }; + + beforeAll(() => { + service = createExternalService( + { + // The trailing slash at the end of the url is intended. + // All API calls need to have the trailing slash removed. + config, + secrets: { apiToken: 'token' }, + }, + logger, + configurationUtilities + ); + }); beforeEach(() => { jest.clearAllMocks(); }); @@ -102,4 +140,121 @@ describe('Swimlane Service', () => { }).toThrow(); }); }); + + describe('getApplication', () => { + test('it returns the fields correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { + fields: [ + ...applicationFields, + { + id: '__proto__', + name: 'unsafe', + key: 'unsafe', + fieldType: 'unsafe', + }, + ], + }, + })); + + const res = await service.getApplication(); + expect(res).toEqual({ fields: applicationFields }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { fields: [] }, + })); + + await service.getApplication(); + expect(requestMock).toHaveBeenCalledWith({ + axios, + logger, + headers, + url: `${config.apiUrl.slice(0, -1)}/api/app/${config.appId}`, + method: 'get', + configurationUtilities, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + await expect(service.getApplication()).rejects.toThrow( + `[Action][Swimlane]: Unable to get application with id ${config.appId}. Error: An error has occurred` + ); + }); + }); + + describe('createRecord', () => { + test('it creates a record correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { + id: '123', + name: 'title', + }, + })); + + const res = await service.createRecord({ + incident, + }); + + expect(res).toEqual({ + id: '123', + title: 'title', + url: `${config.apiUrl.slice(0, -1)}/record/${config.appId}/123`, + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { + id: '123', + name: 'title', + }, + })); + + await service.createRecord({ + incident, + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + logger, + headers, + data: { + applicationId: config.appId, + values: { + [mappings.alertNameConfig.id]: 'Alert Name', + [mappings.alertSourceConfig.id]: 'Alert Source', + [mappings.caseNameConfig.id]: 'Case Name', + [mappings.caseIdConfig.id]: 'Case Id', + [mappings.commentsConfig.id]: 'Comments', + [mappings.severityConfig.id]: 'Severity', + }, + }, + url: `${config.apiUrl.slice(0, -1)}/api/app/${config.appId}/record`, + method: 'post', + configurationUtilities, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + await expect(service.createRecord({ incident })).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Error: An error has occurred` + ); + }); + }); + + // TODO: Implement + describe('updateRecord', () => {}); + + // TODO: Implement + describe('createComment', () => {}); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index cd1ff8d760441..fa964ab90134c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -138,7 +138,7 @@ export const createExternalService = ( url: getPostRecordIdUrl(appId, params.incidentId), }); - const fieldId = mappingConfig.commentsConfig?.id; + const fieldId = getCommentFieldId(mappingConfig); let potentialNewDescription: SwimlaneComment[] = []; let isDescriptionPosted = true; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts new file mode 100644 index 0000000000000..968dc56a1bb8b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { httpServiceMock } from '../../../../../../../../src/core/public/mocks'; +import { getApplication } from './api'; + +const getActionsResponse = { + status: 'ok', + data: { fields: [] }, + actionId: 'te/st', +}; + +describe('Swimlane API', () => { + const http = httpServiceMock.createStartContract(); + + beforeEach(() => jest.resetAllMocks()); + + describe('getApplication', () => { + test('should call getApplication API correctly', async () => { + const abortCtrl = new AbortController(); + http.post.mockResolvedValueOnce(getActionsResponse); + const res = await getApplication({ + http, + signal: abortCtrl.signal, + connectorId: 'te/st', + }); + + expect(res).toEqual(getActionsResponse); + expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', { + body: '{"params":{"subAction":"getApplication","subActionParams":{}}}', + signal: abortCtrl.signal, + }); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts index a84a9df039896..876c78ac303a1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts @@ -6,8 +6,10 @@ */ import { HttpSetup } from 'kibana/public'; +import { ActionTypeExecutorResult } from '../../../../../../actions/common'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { GetApplicationResponse } from '../../../../../../actions/server/builtin_action_types/swimlane/types'; import { BASE_ACTION_API_PATH } from '../../../constants'; -import { SwimlaneFieldMappingConfig } from './types'; export async function getApplication({ http, @@ -17,7 +19,7 @@ export async function getApplication({ http: HttpSetup; signal: AbortSignal; connectorId: string; -}): Promise<{ fields: SwimlaneFieldMappingConfig[] }> { +}): Promise> { return await http.post( `${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`, { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx new file mode 100644 index 0000000000000..38b24c8bf1ca9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx @@ -0,0 +1,176 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; + +import { useKibana } from '../../../../common/lib/kibana'; +import { getApplication } from './api'; +import { SwimlaneActionConnector } from './types'; +import { useGetApplication, UseGetApplication } from './use_get_application'; + +jest.mock('./api'); +jest.mock('../../../../common/lib/kibana'); + +const useKibanaMock = useKibana as jest.Mocked; +const getApplicationMock = getApplication as jest.Mock; + +const action = { + secrets: { apiToken: 'token' }, + id: 'test', + actionTypeId: '.swimlane', + name: 'Swimlane', + isPreconfigured: false, + config: { + apiUrl: 'https://test.swimlane.com/', + appId: 'bcq16kdTbz5jlwM6h', + mappings: {}, + }, +} as SwimlaneActionConnector; + +describe('useGetApplication', () => { + const { services } = useKibanaMock(); + getApplicationMock.mockResolvedValue({ + data: { fields: [] }, + }); + const abortCtrl = new AbortController(); + + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetApplication({ + http: services.http, + action, + toastNotifications: services.notifications.toasts, + }) + ); + + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + getApplication: result.current.getApplication, + }); + }); + }); + + it('calls getApplication with correct arguments', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetApplication({ + http: services.http, + action, + toastNotifications: services.notifications.toasts, + }) + ); + + await waitForNextUpdate(); + + result.current.getApplication(); + await waitForNextUpdate(); + expect(getApplicationMock).toBeCalledWith({ + http: services.http, + connectorId: action.id, + signal: abortCtrl.signal, + }); + }); + }); + + it('get application', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetApplication({ + http: services.http, + action, + toastNotifications: services.notifications.toasts, + }) + ); + + await waitForNextUpdate(); + result.current.getApplication(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + isLoading: false, + getApplication: result.current.getApplication, + }); + }); + }); + + it('set isLoading to true when getting the application', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetApplication({ + http: services.http, + action, + toastNotifications: services.notifications.toasts, + }) + ); + + await waitForNextUpdate(); + result.current.getApplication(); + + expect(result.current.isLoading).toBe(true); + }); + }); + + it('it displays an error when http throws an error', async () => { + getApplicationMock.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetApplication({ + http: services.http, + action, + toastNotifications: services.notifications.toasts, + }) + ); + await waitForNextUpdate(); + result.current.getApplication(); + + expect(result.current).toEqual({ + isLoading: false, + getApplication: result.current.getApplication, + }); + + expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ + title: 'Unable to get application', + text: 'Something went wrong', + }); + }); + }); + + it('it displays an error when service fails', async () => { + getApplicationMock.mockResolvedValue({ + status: 'error', + serviceMessage: 'An error occurred', + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetApplication({ + http: services.http, + action, + toastNotifications: services.notifications.toasts, + }) + ); + await waitForNextUpdate(); + result.current.getApplication(); + await waitForNextUpdate(); + + expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ + title: 'Unable to get application', + text: 'An error occurred', + }); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx index c97098691b9b4..6566dd901822a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx @@ -47,15 +47,17 @@ export const useGetApplication = ({ connectorId: action.id, }); - setIsLoading(false); - if (!isCancelledRef.current) { - toastNotifications.addDanger({ - title: i18n.SW_GET_APPLICATION_API_ERROR, - }); + setIsLoading(false); + if (response.status && response.status === 'error') { + toastNotifications.addDanger({ + title: i18n.SW_GET_APPLICATION_API_ERROR, + text: `${response.serviceMessage ?? response.message}`, + }); + } else { + return response.data; + } } - - return response; } catch (error) { if (!isCancelledRef.current) { if (error.name !== 'AbortError') { From 34dee168a64c31add69ee384aede4a83af5fe489 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 24 May 2021 17:59:13 +0300 Subject: [PATCH 24/96] Revert commit dcaab5b0f5f --- .../builtin_action_types/swimlane/api.ts | 9 ---- .../builtin_action_types/swimlane/index.ts | 8 +--- .../builtin_action_types/swimlane/schema.ts | 5 --- .../builtin_action_types/swimlane/service.ts | 32 +-------------- .../builtin_action_types/swimlane/types.ts | 8 +--- .../connectors/swimlane/case_fields.tsx | 7 +++- .../builtin_action_types/swimlane/api.ts | 41 ++++++++++++------- .../swimlane/steps/swimlane_connection.tsx | 16 +++++--- .../swimlane/swimlane_connectors.tsx | 2 +- .../swimlane/translations.ts | 14 ++++--- .../swimlane/use_get_application.tsx | 16 ++++++-- 11 files changed, 66 insertions(+), 92 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts index 18d4bc286d094..ec6850bf1e786 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts @@ -11,16 +11,8 @@ import { ExternalServiceApi, Incident, PushToServiceApiHandlerArgs, - GetApplicationResponse, - GetApplicationHandlerArgs, } from './types'; -const getApplicationHandler = async ({ - externalService, -}: GetApplicationHandlerArgs): Promise => { - return await externalService.getApplication(); -}; - const createRecordHandler = async ({ externalService, params, @@ -60,7 +52,6 @@ const pushToServiceHandler = async ({ }; export const api: ExternalServiceApi = { - getApplication: getApplicationHandler, createRecord: createRecordHandler, pushToService: pushToServiceHandler, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts index 6de2dd257ecac..5abb7b2e5c5d6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts @@ -33,7 +33,7 @@ interface GetActionTypeParams { configurationUtilities: ActionsConfigurationUtilities; } -const supportedSubActions: string[] = ['getApplication', 'createRecord', 'pushToService']; +const supportedSubActions: string[] = ['application', 'createRecord', 'pushToService']; // action type definition export function getActionType( @@ -101,12 +101,6 @@ async function executor( throw new Error(errorMessage); } - if (subAction === 'getApplication') { - data = await api.getApplication({ - externalService, - }); - } - if (subAction === 'createRecord') { const createRecordParams = subActionParams as ExecutorSubActionCreateRecordParams; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts index 59df49e63cc5f..86c832832ae5f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -50,7 +50,6 @@ const SwimlaneFields = { severity: schema.nullable(schema.string()), }; -export const ExecutorSubActionGetApplicationParamsSchema = schema.object({}); export const ExecutorSubActionCreateRecordParamsSchema = schema.object(SwimlaneFields); export const ExecutorSubActionPushParamsSchema = schema.object({ @@ -69,10 +68,6 @@ export const ExecutorSubActionPushParamsSchema = schema.object({ }); export const ExecutorParamsSchema = schema.oneOf([ - schema.object({ - subAction: schema.literal('getApplication'), - subActionParams: ExecutorSubActionGetApplicationParamsSchema, - }), schema.object({ subAction: schema.literal('createRecord'), subActionParams: ExecutorSubActionCreateRecordParamsSchema, diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index fa964ab90134c..48217a69b9c7c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -10,18 +10,13 @@ import axios from 'axios'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { getErrorMessage, request } from '../lib/axios_utils'; -import { - getBodyForEventAction, - removeCommentFieldUpdatedInformation, - removeUnsafeFields, -} from './helpers'; +import { getBodyForEventAction, removeCommentFieldUpdatedInformation } from './helpers'; import { CreateCommentParams, CreateRecordParams, ExternalService, ExternalServiceCredentials, ExternalServiceIncidentResponse, - GetApplicationResponse, MappingConfigType, SwimlaneComment, SwimlanePublicConfigurationType, @@ -56,8 +51,6 @@ export const createExternalService = ( ? urlWithoutTrailingSlash : urlWithoutTrailingSlash + '/api'; - const applicationUrl = `${apiUrl}/app/${appId}`; - const getPostRecordUrl = (id: string) => `${apiUrl}/app/${id}/record`; const getPostRecordIdUrl = (id: string, recordId: string) => @@ -72,28 +65,6 @@ export const createExternalService = ( const getCommentFieldId = (fieldMappings: MappingConfigType): string | null => fieldMappings.commentsConfig?.id || null; - const getApplication = async (): Promise => { - try { - const res = await request({ - axios: axiosInstance, - configurationUtilities, - headers, - logger, - method: 'get', - url: applicationUrl, - }); - - return { ...res.data, fields: removeUnsafeFields(res.data?.fields ?? []) }; - } catch (error) { - throw new Error( - getErrorMessage( - i18n.NAME, - `Unable to get application with id ${appId}. Error: ${error.message}` - ) - ); - } - }; - const createRecord = async ( params: CreateRecordParams ): Promise => { @@ -238,6 +209,5 @@ export const createExternalService = ( createComment, createRecord, updateRecord, - getApplication, }; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts index 3ef384a660fa4..92cd121dc4717 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts @@ -70,10 +70,6 @@ export interface FieldConfig { fieldType: string; } -export interface GetApplicationResponse { - fields: FieldConfig[]; -} - export interface SwimlaneRecordPayload { applicationId: string; id?: string; @@ -82,7 +78,6 @@ export interface SwimlaneRecordPayload { } export interface ExternalService { - getApplication: () => Promise; createComment: (params: CreateCommentParams) => Promise; createRecord: (params: CreateRecordParams) => Promise; updateRecord: (params: UpdateRecordParams) => Promise; @@ -106,12 +101,11 @@ export interface GetApplicationHandlerArgs { } export interface ExternalServiceApi { - getApplication: (args: GetApplicationHandlerArgs) => Promise; createRecord: (args: CreateRecordApiHandlerArgs) => Promise; pushToService: (args: PushToServiceApiHandlerArgs) => Promise; } -export type SwimlaneExecutorResultData = ExternalServiceIncidentResponse | GetApplicationResponse; +export type SwimlaneExecutorResultData = ExternalServiceIncidentResponse; export type SwimlaneDataValues = Record; export interface SwimlaneComment { fieldId: string; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx index d896e21f89d2f..79c08c3be16c6 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx @@ -15,7 +15,12 @@ import { fieldLabels } from './index'; const SwimlaneFieldsComponent: React.FunctionComponent< ConnectorFieldsProps > = ({ isEdit = true, fields, connector, onChange }) => { - const { alertSource = null, caseId = null, caseName = null, severity = null } = fields ?? {}; + const { alertSource, caseId, caseName, severity } = fields || { + alertSource: null, + caseId: null, + caseName: null, + severity: null, + }; const onFieldChange = useCallback( (key, value) => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts index 876c78ac303a1..140f0469bfb77 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts @@ -6,27 +6,38 @@ */ import { HttpSetup } from 'kibana/public'; -import { ActionTypeExecutorResult } from '../../../../../../actions/common'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { GetApplicationResponse } from '../../../../../../actions/server/builtin_action_types/swimlane/types'; -import { BASE_ACTION_API_PATH } from '../../../constants'; export async function getApplication({ http, signal, - connectorId, + url, + appId, + apiToken, }: { http: HttpSetup; signal: AbortSignal; - connectorId: string; -}): Promise> { - return await http.post( - `${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`, - { - body: JSON.stringify({ - params: { subAction: 'getApplication', subActionParams: {} }, - }), + url: string; + appId: string; + apiToken: string; +}): Promise> { + const headers: Record = { + 'Content-Type': 'application/json', + 'Private-Token': `${apiToken}`, + }; + + const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url; + const apiUrl = urlWithoutTrailingSlash.endsWith('api') + ? urlWithoutTrailingSlash + : urlWithoutTrailingSlash + '/api'; + const applicationUrl = `${apiUrl}/app/{appId}`; + + const getApplicationUrl = (id: string) => applicationUrl.replace('{appId}', id); + try { + return await http.get(getApplicationUrl(appId), { + headers, signal, - } - ); + }); + } catch (error) { + throw new Error(`Unable to get application with id ${appId}. Error: ${error.message}`); + } } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx index a35af7ce111a6..733a492e54e40 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx @@ -40,6 +40,9 @@ export const SwimlaneConnection: React.FunctionComponent = ({ http, toastNotifications: toasts, action, + apiToken, + appId, + apiUrl, }); const isValid = useMemo(() => apiUrl && apiToken && appId, [apiToken, apiUrl, appId]); @@ -47,12 +50,13 @@ export const SwimlaneConnection: React.FunctionComponent = ({ // fetch swimlane application configuration const application = await getApplication(); - if (application != null) { - const allFields = application.fields; - updateFields(allFields); - updateCurrentStep(2); + if (!application) { + throw new Error(i18n.SW_GET_APPLICATION_API_ERROR(appId)); } - }, [getApplication, updateCurrentStep, updateFields]); + const allFields = application.fields; + updateFields(allFields); + updateCurrentStep(2); + }, [appId, getApplication, updateCurrentStep, updateFields]); const onChangeConfig = useCallback( (e: React.ChangeEvent, key: 'apiUrl' | 'appId') => { @@ -151,7 +155,7 @@ export const SwimlaneConnection: React.FunctionComponent = ({ fullWidth isInvalid={isInvalid} readOnly={readOnly} - value={apiToken ?? ''} + value={apiToken || ''} data-test-subj="swimlaneApiTokenInput" onChange={onChangeSecrets} onBlur={onBlurSecrets} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx index 01cae4c7de96b..6bf2bae37765c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx @@ -12,7 +12,7 @@ import { ActionConnectorFieldsProps } from '../../../../types'; import { SwimlaneActionConnector, SwimlaneFieldMappingConfig } from './types'; import { SwimlaneConnection, SwimlaneFields } from './steps'; -const SwimlaneActionConnectorFields: React.FC< +const SwimlaneActionConnectorFields: React.FunctionComponent< ActionConnectorFieldsProps > = ({ errors, action, editActionConfig, editActionSecrets, readOnly }) => { const [currentStep, setCurrentStep] = useState(1); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts index 74dd783e1d191..61dee9e031ccd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts @@ -49,12 +49,14 @@ export const SW_REQUIRED_API_TOKEN_TEXT = i18n.translate( } ); -export const SW_GET_APPLICATION_API_ERROR = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.swimlane.unableToGetApplicationMessage', - { - defaultMessage: 'Unable to get application', - } -); +export const SW_GET_APPLICATION_API_ERROR = (id: string | null) => + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlane.unableToGetApplicationMessage', + { + defaultMessage: 'Unable to get application with id {id}', + values: { id }, + } + ); export const SW_API_URL_TEXT_FIELD_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiUrlTextFieldLabel', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx index 6566dd901822a..8a0f74100a271 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx @@ -18,6 +18,9 @@ interface Props { 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' >; action: SwimlaneActionConnector; + appId: string; + apiToken: string; + apiUrl: string; } export interface UseGetApplication { @@ -29,6 +32,9 @@ export const useGetApplication = ({ http, action, toastNotifications, + appId, + apiToken, + apiUrl, }: Props): UseGetApplication => { const [isLoading, setIsLoading] = useState(false); const isCancelledRef = useRef(false); @@ -44,14 +50,16 @@ export const useGetApplication = ({ const response = await getApplicationApi({ http, signal: abortCtrlRef.current.signal, - connectorId: action.id, + appId, + apiToken, + url: apiUrl, }); if (!isCancelledRef.current) { setIsLoading(false); if (response.status && response.status === 'error') { toastNotifications.addDanger({ - title: i18n.SW_GET_APPLICATION_API_ERROR, + title: i18n.SW_GET_APPLICATION_API_ERROR(appId), text: `${response.serviceMessage ?? response.message}`, }); } else { @@ -62,14 +70,14 @@ export const useGetApplication = ({ if (!isCancelledRef.current) { if (error.name !== 'AbortError') { toastNotifications.addDanger({ - title: i18n.SW_GET_APPLICATION_API_ERROR, + title: i18n.SW_GET_APPLICATION_API_ERROR(appId), text: error.message, }); } setIsLoading(false); } } - }, [action, http, toastNotifications]); + }, [apiToken, apiUrl, appId, http, toastNotifications]); return { isLoading, From 32d76d75368cacf49eea7c23f1d2b17030382b4f Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Mon, 24 May 2021 17:16:20 -0400 Subject: [PATCH 25/96] Fixing logout issue --- .../builtin_action_types/swimlane/api.test.ts | 72 +++++++++++++++---- .../builtin_action_types/swimlane/api.ts | 15 ++-- .../swimlane/steps/swimlane_connection.tsx | 11 ++- .../swimlane/translations.ts | 7 ++ .../swimlane/use_get_application.test.tsx | 48 +++++++------ .../swimlane/use_get_application.tsx | 24 +++---- 6 files changed, 117 insertions(+), 60 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts index 968dc56a1bb8b..abb6506a65810 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts @@ -5,35 +5,81 @@ * 2.0. */ -import { httpServiceMock } from '../../../../../../../../src/core/public/mocks'; import { getApplication } from './api'; const getActionsResponse = { - status: 'ok', - data: { fields: [] }, - actionId: 'te/st', + fields: [], }; describe('Swimlane API', () => { - const http = httpServiceMock.createStartContract(); + let fetchMock: jest.SpyInstance>; - beforeEach(() => jest.resetAllMocks()); + beforeAll(() => jest.spyOn(window, 'fetch')); + beforeEach(() => { + jest.resetAllMocks(); + fetchMock = jest.spyOn(window, 'fetch'); + }); describe('getApplication', () => { - test('should call getApplication API correctly', async () => { + it('should call getApplication API correctly', async () => { const abortCtrl = new AbortController(); - http.post.mockResolvedValueOnce(getActionsResponse); + + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => getActionsResponse, + }); const res = await getApplication({ - http, signal: abortCtrl.signal, - connectorId: 'te/st', + apiToken: '', + appId: '', + url: '', }); expect(res).toEqual(getActionsResponse); - expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', { - body: '{"params":{"subAction":"getApplication","subActionParams":{}}}', - signal: abortCtrl.signal, + }); + + it('returns an error when the response fails', async () => { + const abortCtrl = new AbortController(); + + fetchMock.mockResolvedValueOnce({ + ok: false, + status: 401, + json: async () => getActionsResponse, }); + + try { + await getApplication({ + signal: abortCtrl.signal, + apiToken: '', + appId: '', + url: '', + }); + } catch (e) { + expect(e.message).toContain('Received status:'); + } + }); + + it('returns an error when parsing the json fails', async () => { + const abortCtrl = new AbortController(); + + fetchMock.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => { + throw new Error('bad'); + }, + }); + + try { + await getApplication({ + signal: abortCtrl.signal, + apiToken: '', + appId: '', + url: '', + }); + } catch (e) { + expect(e.message).toContain('bad'); + } }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts index 140f0469bfb77..4ddf8c2aa1038 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts @@ -5,16 +5,12 @@ * 2.0. */ -import { HttpSetup } from 'kibana/public'; - export async function getApplication({ - http, signal, url, appId, apiToken, }: { - http: HttpSetup; signal: AbortSignal; url: string; appId: string; @@ -33,10 +29,19 @@ export async function getApplication({ const getApplicationUrl = (id: string) => applicationUrl.replace('{appId}', id); try { - return await http.get(getApplicationUrl(appId), { + const response = await window.fetch(getApplicationUrl(appId), { + method: 'GET', headers, signal, }); + + if (!response.ok) { + throw new Error( + `Received status: ${response.status} when attempting to get application with id: ${appId}` + ); + } + + return await response.json(); } catch (error) { throw new Error(`Unable to get application with id ${appId}. Error: ${error.message}`); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx index 733a492e54e40..33b9919652564 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx @@ -30,16 +30,13 @@ export const SwimlaneConnection: React.FunctionComponent = ({ updateFields, }) => { const { - http, notifications: { toasts }, } = useKibana().services; const { apiUrl, appId } = action.config; const { apiToken } = action.secrets; const { docLinks } = useKibana().services; const { getApplication } = useGetApplication({ - http, toastNotifications: toasts, - action, apiToken, appId, apiUrl, @@ -50,13 +47,15 @@ export const SwimlaneConnection: React.FunctionComponent = ({ // fetch swimlane application configuration const application = await getApplication(); - if (!application) { - throw new Error(i18n.SW_GET_APPLICATION_API_ERROR(appId)); + if (!application?.fields) { + // Error has already been surfaced within the getApplication call by a toast + return; } + const allFields = application.fields; updateFields(allFields); updateCurrentStep(2); - }, [appId, getApplication, updateCurrentStep, updateFields]); + }, [getApplication, updateCurrentStep, updateFields]); const onChangeConfig = useCallback( (e: React.ChangeEvent, key: 'apiUrl' | 'appId') => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts index 61dee9e031ccd..2f7ea8d368fa4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts @@ -58,6 +58,13 @@ export const SW_GET_APPLICATION_API_ERROR = (id: string | null) => } ); +export const SW_GET_APPLICATION_API_NO_FIELDS_ERROR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlane.unableToGetApplicationFieldsMessage', + { + defaultMessage: 'Unable to get application fields', + } +); + export const SW_API_URL_TEXT_FIELD_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiUrlTextFieldLabel', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx index 38b24c8bf1ca9..4744c4d22fdc9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx @@ -47,8 +47,9 @@ describe('useGetApplication', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useGetApplication({ - http: services.http, - action, + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, toastNotifications: services.notifications.toasts, }) ); @@ -65,8 +66,9 @@ describe('useGetApplication', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useGetApplication({ - http: services.http, - action, + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, toastNotifications: services.notifications.toasts, }) ); @@ -76,9 +78,10 @@ describe('useGetApplication', () => { result.current.getApplication(); await waitForNextUpdate(); expect(getApplicationMock).toBeCalledWith({ - http: services.http, - connectorId: action.id, signal: abortCtrl.signal, + appId: action.config.appId, + apiToken: action.secrets.apiToken, + url: action.config.apiUrl, }); }); }); @@ -87,8 +90,9 @@ describe('useGetApplication', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useGetApplication({ - http: services.http, - action, + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, toastNotifications: services.notifications.toasts, }) ); @@ -108,8 +112,9 @@ describe('useGetApplication', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useGetApplication({ - http: services.http, - action, + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, toastNotifications: services.notifications.toasts, }) ); @@ -129,8 +134,9 @@ describe('useGetApplication', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useGetApplication({ - http: services.http, - action, + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, toastNotifications: services.notifications.toasts, }) ); @@ -143,23 +149,21 @@ describe('useGetApplication', () => { }); expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ - title: 'Unable to get application', + title: 'Unable to get application with id bcq16kdTbz5jlwM6h', text: 'Something went wrong', }); }); }); - it('it displays an error when service fails', async () => { - getApplicationMock.mockResolvedValue({ - status: 'error', - serviceMessage: 'An error occurred', - }); + it('it displays an error when the response does not contain the correct fields', async () => { + getApplicationMock.mockResolvedValue({}); await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useGetApplication({ - http: services.http, - action, + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, toastNotifications: services.notifications.toasts, }) ); @@ -168,8 +172,8 @@ describe('useGetApplication', () => { await waitForNextUpdate(); expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ - title: 'Unable to get application', - text: 'An error occurred', + title: 'Unable to get application with id bcq16kdTbz5jlwM6h', + text: 'Unable to get application fields', }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx index 8a0f74100a271..f18770067b8a8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx @@ -6,31 +6,27 @@ */ import { useState, useCallback, useRef } from 'react'; -import { HttpSetup, ToastsApi } from 'kibana/public'; +import { ToastsApi } from 'kibana/public'; import { getApplication as getApplicationApi } from './api'; import * as i18n from './translations'; -import { SwimlaneActionConnector, SwimlaneFieldMappingConfig } from './types'; +import { SwimlaneFieldMappingConfig } from './types'; interface Props { - http: HttpSetup; toastNotifications: Pick< ToastsApi, 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' >; - action: SwimlaneActionConnector; appId: string; apiToken: string; apiUrl: string; } export interface UseGetApplication { - getApplication: () => Promise<{ fields: SwimlaneFieldMappingConfig[] } | undefined>; + getApplication: () => Promise<{ fields?: SwimlaneFieldMappingConfig[] } | undefined>; isLoading: boolean; } export const useGetApplication = ({ - http, - action, toastNotifications, appId, apiToken, @@ -47,8 +43,7 @@ export const useGetApplication = ({ abortCtrlRef.current = new AbortController(); setIsLoading(true); - const response = await getApplicationApi({ - http, + const data = await getApplicationApi({ signal: abortCtrlRef.current.signal, appId, apiToken, @@ -57,14 +52,15 @@ export const useGetApplication = ({ if (!isCancelledRef.current) { setIsLoading(false); - if (response.status && response.status === 'error') { + if (!data.fields) { + // If the response was malformed and fields doesn't exist, show an error toast toastNotifications.addDanger({ title: i18n.SW_GET_APPLICATION_API_ERROR(appId), - text: `${response.serviceMessage ?? response.message}`, + text: i18n.SW_GET_APPLICATION_API_NO_FIELDS_ERROR, }); - } else { - return response.data; + return; } + return data; } } catch (error) { if (!isCancelledRef.current) { @@ -77,7 +73,7 @@ export const useGetApplication = ({ setIsLoading(false); } } - }, [apiToken, apiUrl, appId, http, toastNotifications]); + }, [apiToken, apiUrl, appId, toastNotifications]); return { isLoading, From b51be9fb12fb0580b8b07127c3de54d50dc8fc71 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 25 May 2021 20:03:31 +0300 Subject: [PATCH 26/96] Minor improvements --- .../builtin_action_types/swimlane/api.test.ts | 8 +++--- .../builtin_action_types/swimlane/api.ts | 2 +- .../swimlane/steps/swimlane_connection.tsx | 26 +++++++++---------- .../swimlane/steps/swimlane_fields.tsx | 1 + .../swimlane/swimlane_connectors.tsx | 1 + 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts index abb6506a65810..5919ea0bc8215 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts @@ -7,7 +7,7 @@ import { getApplication } from './api'; -const getActionsResponse = { +const getApplicationResponse = { fields: [], }; @@ -26,7 +26,7 @@ describe('Swimlane API', () => { fetchMock.mockResolvedValueOnce({ ok: true, - json: async () => getActionsResponse, + json: async () => getApplicationResponse, }); const res = await getApplication({ signal: abortCtrl.signal, @@ -35,7 +35,7 @@ describe('Swimlane API', () => { url: '', }); - expect(res).toEqual(getActionsResponse); + expect(res).toEqual(getApplicationResponse); }); it('returns an error when the response fails', async () => { @@ -44,7 +44,7 @@ describe('Swimlane API', () => { fetchMock.mockResolvedValueOnce({ ok: false, status: 401, - json: async () => getActionsResponse, + json: async () => getApplicationResponse, }); try { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts index 4ddf8c2aa1038..c6e2675403458 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts @@ -29,7 +29,7 @@ export async function getApplication({ const getApplicationUrl = (id: string) => applicationUrl.replace('{appId}', id); try { - const response = await window.fetch(getApplicationUrl(appId), { + const response = await fetch(getApplicationUrl(appId), { method: 'GET', headers, signal, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx index 33b9919652564..90878f54c1a89 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx @@ -47,14 +47,11 @@ export const SwimlaneConnection: React.FunctionComponent = ({ // fetch swimlane application configuration const application = await getApplication(); - if (!application?.fields) { - // Error has already been surfaced within the getApplication call by a toast - return; + if (application != null && application.fields) { + const allFields = application.fields; + updateFields(allFields); + updateCurrentStep(2); } - - const allFields = application.fields; - updateFields(allFields); - updateCurrentStep(2); }, [getApplication, updateCurrentStep, updateFields]); const onChangeConfig = useCallback( @@ -63,6 +60,7 @@ export const SwimlaneConnection: React.FunctionComponent = ({ }, [editActionConfig] ); + const onBlurConfig = useCallback( (key: 'apiUrl' | 'appId') => { if (!action.config[key]) { @@ -71,29 +69,29 @@ export const SwimlaneConnection: React.FunctionComponent = ({ }, [action.config, editActionConfig] ); + const onChangeSecrets = useCallback( (e: React.ChangeEvent) => { editActionSecrets('apiToken', e.target.value); }, [editActionSecrets] ); + const onBlurSecrets = useCallback(() => { if (!apiToken) { editActionSecrets('apiToken', ''); } }, [apiToken, editActionSecrets]); - const isInvalid = useMemo(() => errors.apiToken.length > 0 && apiToken !== undefined, [ - apiToken, - errors.apiToken.length, - ]); + const isInvalid = errors.apiToken.length > 0 && apiToken !== undefined; + return ( <> onChangeConfig(e, 'apiUrl')} @@ -104,7 +102,7 @@ export const SwimlaneConnection: React.FunctionComponent = ({ onChangeConfig(e, 'appId')} @@ -154,7 +152,7 @@ export const SwimlaneConnection: React.FunctionComponent = ({ fullWidth isInvalid={isInvalid} readOnly={readOnly} - value={apiToken || ''} + value={apiToken ?? ''} data-test-subj="swimlaneApiTokenInput" onChange={onChangeSecrets} onBlur={onBlurSecrets} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index 3383b7eeaecda..1b5124bd54f9b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -12,6 +12,7 @@ import { StepProps } from './'; const SINGLE_SELECTION = { asPlainText: true }; const empty = { label: '', value: '' }; + const findOption = ( options: Array>, searchValue: string diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx index 6bf2bae37765c..9cb2857780e37 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx @@ -24,6 +24,7 @@ const SwimlaneActionConnectorFields: React.FunctionComponent< }), [] ); + const CurrentStepForm = useMemo(() => { return stepMap[currentStep]; }, [currentStep, stepMap]); From abba08c9ed1ddc1b6a5ab68dc682d7fefe96ed8f Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 26 May 2021 14:41:48 +0300 Subject: [PATCH 27/96] Prevent prototype polution from fields --- .../builtin_action_types/swimlane/api.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts index c6e2675403458..1a0f93ef96197 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts @@ -5,6 +5,16 @@ * 2.0. */ +import { SwimlaneFieldMappingConfig } from './types'; + +const removeUnsafeFields = (fields: SwimlaneFieldMappingConfig[]): SwimlaneFieldMappingConfig[] => + fields.filter( + (filter) => + filter.id !== '__proto__' && + filter.key !== '__proto__' && + filter.name !== '__proto__' && + filter.fieldType !== '__proto__' + ); export async function getApplication({ signal, url, @@ -28,6 +38,7 @@ export async function getApplication({ const applicationUrl = `${apiUrl}/app/{appId}`; const getApplicationUrl = (id: string) => applicationUrl.replace('{appId}', id); + try { const response = await fetch(getApplicationUrl(appId), { method: 'GET', @@ -35,13 +46,19 @@ export async function getApplication({ signal, }); + /** + * Fetch does not throws when there is an HTTP error (status >= 400). + * We need to do it manually. + */ + if (!response.ok) { throw new Error( `Received status: ${response.status} when attempting to get application with id: ${appId}` ); } - return await response.json(); + const data = await response.json(); + return { ...data, fields: removeUnsafeFields(data?.fields ?? []) }; } catch (error) { throw new Error(`Unable to get application with id ${appId}. Error: ${error.message}`); } From e15c83c926274a96389e1e6cdf5db477c9835122 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 26 May 2021 14:50:15 +0300 Subject: [PATCH 28/96] Add description field --- .../server/builtin_action_types/swimlane/schema.ts | 2 ++ .../swimlane/steps/swimlane_fields.tsx | 11 +++++++++++ .../builtin_action_types/swimlane/translations.ts | 7 +++++++ .../components/builtin_action_types/swimlane/types.ts | 1 + 4 files changed, 21 insertions(+) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts index 86c832832ae5f..f4662033d5b2c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -23,6 +23,7 @@ export const ConfigMapping = { caseNameConfig: schema.nullable(ConfigMapSchema), commentsConfig: schema.nullable(ConfigMapSchema), severityConfig: schema.nullable(ConfigMapSchema), + descriptionConfig: schema.nullable(ConfigMapSchema), }; export const ConfigMappingSchema = schema.object(ConfigMapping); @@ -48,6 +49,7 @@ const SwimlaneFields = { caseName: schema.nullable(schema.string()), comments: schema.nullable(schema.string()), severity: schema.nullable(schema.string()), + description: schema.nullable(schema.string()), }; export const ExecutorSubActionCreateRecordParamsSchema = schema.object(SwimlaneFields); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index 1b5124bd54f9b..732b359de15a8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -48,6 +48,7 @@ export const SwimlaneFields: React.FunctionComponent = ({ caseIdConfig: findOption(options, mappings?.caseIdConfig?.id), caseNameConfig: findOption(options, mappings?.caseNameConfig?.id), commentsConfig: findOption(options, mappings?.commentsConfig?.id), + descriptionConfig: findOption(options, mappings?.descriptionConfig?.id), }), [options, mappings] ); @@ -136,6 +137,16 @@ export const SwimlaneFields: React.FunctionComponent = ({ onChange={(e) => editMappings('commentsConfig', e)} /> + + editMappings('descriptionConfig', e)} + /> + {i18n.SW_RETRIEVE_CONFIGURATION_RESET_LABEL} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts index 2f7ea8d368fa4..0d2160e678b5b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts @@ -156,6 +156,13 @@ export const SW_COMMENTS_FIELD_LABEL = i18n.translate( } ); +export const SW_DESCRIPTION_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.descriptionFieldLabel', + { + defaultMessage: 'Description', + } +); + export const SW_REMEMBER_VALUE_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.rememberValueLabel', { defaultMessage: 'Remember this value. You must reenter it each time you edit the connector.' } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts index 184abe358a8d2..254c616e969f2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts @@ -27,6 +27,7 @@ export interface SwimlaneMappingConfig { caseIdConfig: SwimlaneFieldMappingConfig; alertNameConfig: SwimlaneFieldMappingConfig; commentsConfig: SwimlaneFieldMappingConfig; + descriptionConfig: SwimlaneFieldMappingConfig; } export interface SwimlaneFieldMappingConfig { From 97892b2c02f90a005a7c8b477d74c41f7e860ede Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 26 May 2021 14:56:24 +0300 Subject: [PATCH 29/96] Make alert name optional --- .../actions/server/builtin_action_types/swimlane/schema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts index f4662033d5b2c..0a9444ca6dde6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -17,7 +17,7 @@ export const ConfigMap = { export const ConfigMapSchema = schema.object(ConfigMap); export const ConfigMapping = { - alertNameConfig: ConfigMapSchema, + alertNameConfig: schema.nullable(ConfigMapSchema), alertSourceConfig: schema.nullable(ConfigMapSchema), caseIdConfig: schema.nullable(ConfigMapSchema), caseNameConfig: schema.nullable(ConfigMapSchema), @@ -43,7 +43,7 @@ export const SwimlaneSecretsConfiguration = { export const SwimlaneSecretsConfigurationSchema = schema.object(SwimlaneSecretsConfiguration); const SwimlaneFields = { - alertName: schema.string(), + alertName: schema.nullable(schema.string()), alertSource: schema.nullable(schema.string()), caseId: schema.nullable(schema.string()), caseName: schema.nullable(schema.string()), From aff100044a0c2af9a6bc559869227dcacecb813e Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 26 May 2021 15:00:26 +0300 Subject: [PATCH 30/96] Change case mapping --- x-pack/plugins/cases/server/client/configure/utils.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/cases/server/client/configure/utils.ts b/x-pack/plugins/cases/server/client/configure/utils.ts index 9f1fa7f936c94..a5b697ac0e6fd 100644 --- a/x-pack/plugins/cases/server/client/configure/utils.ts +++ b/x-pack/plugins/cases/server/client/configure/utils.ts @@ -61,15 +61,15 @@ const normalizeServiceNowFields = (snFields: ServiceNowGetFieldsResponse): Conne [] ); export interface SwimlaneMappings { - title: string; + title?: string; description?: string; comments?: string; } export const mapSwimlaneFields = ( slFields: SwimlanePublicConfigurationType['mappings'] ): SwimlaneMappings => ({ - title: slFields.alertNameConfig.id, - description: slFields.commentsConfig?.id, + title: slFields.caseNameConfig?.id, + description: slFields.descriptionConfig?.id, comments: slFields.commentsConfig?.id, }); @@ -111,9 +111,8 @@ const getPreferredFields = (theType: string, swimlaneMappings?: SwimlaneMappings description = 'description'; comments = 'work_notes'; } else if (theType === ConnectorTypes.swimlane) { - title = 'alertName'; - // TODO: Where we should map the description? - description = 'comments'; + title = 'caseName'; + description = 'description'; comments = 'comments'; } From 15e442f4c6048d8dfc747a2ca24a5ac445b9a5b3 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 26 May 2021 15:14:16 +0300 Subject: [PATCH 31/96] Remove cases fields UI --- .../cases/common/api/connectors/swimlane.ts | 2 - .../connectors/swimlane/case_fields.test.tsx | 50 --------- .../connectors/swimlane/case_fields.tsx | 105 ------------------ .../components/connectors/swimlane/index.ts | 4 +- .../cases/public/components/create/schema.tsx | 4 +- .../cases/server/connectors/case/schema.ts | 12 +- .../swimlane/external_service_formatter.ts | 4 +- 7 files changed, 11 insertions(+), 170 deletions(-) delete mode 100644 x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx delete mode 100644 x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx diff --git a/x-pack/plugins/cases/common/api/connectors/swimlane.ts b/x-pack/plugins/cases/common/api/connectors/swimlane.ts index 2f36a5d554d92..b1fce5171adf9 100644 --- a/x-pack/plugins/cases/common/api/connectors/swimlane.ts +++ b/x-pack/plugins/cases/common/api/connectors/swimlane.ts @@ -9,10 +9,8 @@ import * as rt from 'io-ts'; // New fields should also be added at: x-pack/plugins/cases/server/connectors/case/schema.ts export const SwimlaneFieldsRT = rt.type({ - alertSource: rt.union([rt.string, rt.null]), caseId: rt.union([rt.string, rt.null]), caseName: rt.union([rt.string, rt.null]), - severity: rt.union([rt.string, rt.null]), }); export type SwimlaneFieldsType = rt.TypeOf; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx deleted file mode 100644 index cd11e27d61747..0000000000000 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mount } from 'enzyme'; - -import { connector } from '../mock'; -import Fields from './case_fields'; - -jest.mock('../../../common/lib/kibana'); - -describe('SwimlaneParamsFields renders', () => { - const fields = { - alertSource: '1', - caseId: '2', - caseName: '3', - severity: '4', - }; - - const onChange = jest.fn(); - beforeEach(() => { - jest.clearAllMocks(); - }); - - Object.entries(fields).forEach(([k, v]) => - describe(`${k} tests`, () => { - test(`param field is rendered`, () => { - const wrapper = mount(); - expect(wrapper.find(`[data-test-subj="${k}"]`).first().prop('value')).toStrictEqual(v); - }); - - test(`onChange is called`, () => { - const wrapper = mount(); - const newValue = 'wowie'; - wrapper - .find(`input[data-test-subj="${k}"]`) - .first() - .simulate('change', { - target: { value: newValue }, - }); - - expect(onChange).toHaveBeenCalledWith({ ...fields, [k]: newValue }); - }); - }) - ); -}); diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx deleted file mode 100644 index 79c08c3be16c6..0000000000000 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useMemo, useCallback } from 'react'; -import { EuiFieldText, EuiFormRow, EuiSpacer } from '@elastic/eui'; - -import { ConnectorFieldsProps } from '../types'; -import { ConnectorTypes, SwimlaneFieldsType } from '../../../../common'; -import { ConnectorCard } from '../card'; -import { fieldLabels } from './index'; -const SwimlaneFieldsComponent: React.FunctionComponent< - ConnectorFieldsProps -> = ({ isEdit = true, fields, connector, onChange }) => { - const { alertSource, caseId, caseName, severity } = fields || { - alertSource: null, - caseId: null, - caseName: null, - severity: null, - }; - - const onFieldChange = useCallback( - (key, value) => { - onChange({ - ...fields, - alertSource, - caseId, - caseName, - severity, - [key]: value, - }); - }, - [alertSource, caseId, caseName, fields, onChange, severity] - ); - const listItems = useMemo( - () => - Object.entries({ alertSource, caseId, caseName, severity }).reduce( - (acc: Array<{ title: string; description: string }>, [key, value]) => { - const fieldName = key as keyof SwimlaneFieldsType; - return [ - ...acc, - ...(value !== null && value !== '' - ? [ - { - title: fieldLabels[fieldName], - description: value ?? '', - }, - ] - : []), - ]; - }, - [] - ), - [alertSource, caseId, caseName, severity] - ); - return isEdit ? ( - - - onFieldChange('alertSource', e.target.value)} - /> - - - - onFieldChange('caseId', e.target.value)} - /> - - - - onFieldChange('caseName', e.target.value)} - /> - - - - onFieldChange('severity', e.target.value)} - /> - - - - ) : ( - - ); -}; - -// eslint-disable-next-line import/no-default-export -export { SwimlaneFieldsComponent as default }; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts index 0503266ba3523..df949151496b2 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { lazy } from 'react'; - import { CaseConnector } from '../types'; import { ConnectorTypes, SwimlaneFieldsType } from '../../../../common'; import * as i18n from './translations'; @@ -14,7 +12,7 @@ import * as i18n from './translations'; export const getCaseConnector = (): CaseConnector => { return { id: ConnectorTypes.swimlane, - fieldsComponent: lazy(() => import('./case_fields')), + fieldsComponent: null, }; }; diff --git a/x-pack/plugins/cases/public/components/create/schema.tsx b/x-pack/plugins/cases/public/components/create/schema.tsx index 7ca1e2e061545..c20fa5c672943 100644 --- a/x-pack/plugins/cases/public/components/create/schema.tsx +++ b/x-pack/plugins/cases/public/components/create/schema.tsx @@ -49,7 +49,9 @@ export const schema: FormSchema = { label: i18n.CONNECTORS, defaultValue: 'none', }, - fields: {}, + fields: { + defaultValue: null, + }, syncAlerts: { helpText: i18n.SYNC_ALERTS_HELP, type: FIELD_TYPES.TOGGLE, diff --git a/x-pack/plugins/cases/server/connectors/case/schema.ts b/x-pack/plugins/cases/server/connectors/case/schema.ts index ce1202cade335..11e6587db5f7f 100644 --- a/x-pack/plugins/cases/server/connectors/case/schema.ts +++ b/x-pack/plugins/cases/server/connectors/case/schema.ts @@ -51,13 +51,6 @@ const JiraFieldsSchema = schema.object({ parent: schema.nullable(schema.string()), }); -const SwimlaneFieldsSchema = schema.object({ - alertSource: schema.nullable(schema.string()), - caseId: schema.nullable(schema.string()), - caseName: schema.nullable(schema.string()), - severity: schema.nullable(schema.string()), -}); - const ResilientFieldsSchema = schema.object({ incidentTypes: schema.nullable(schema.arrayOf(schema.string())), severityCode: schema.nullable(schema.string()), @@ -81,6 +74,11 @@ const ServiceNowSIRFieldsSchema = schema.object({ subcategory: schema.nullable(schema.string()), }); +const SwimlaneFieldsSchema = schema.object({ + caseId: schema.nullable(schema.string()), + caseName: schema.nullable(schema.string()), +}); + const NoneFieldsSchema = schema.nullable(schema.object({})); const ReducedConnectorFieldsSchema: { [x: string]: any } = { diff --git a/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts b/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts index d0f49cef9f17f..cfd923d4f3c31 100644 --- a/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts +++ b/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts @@ -9,9 +9,9 @@ import { ExternalServiceFormatter } from '../types'; import { ConnectorSwimlaneTypeFields, SwimlaneFieldsType } from '../../../common'; const format: ExternalServiceFormatter['format'] = (theCase) => { - const { alertSource = null, caseId = null, caseName = null, severity = null } = + const { caseId = theCase.id, caseName = theCase.title } = (theCase.connector.fields as ConnectorSwimlaneTypeFields['fields']) ?? {}; - return { alertSource, caseId, caseName, severity }; + return { caseId, caseName }; }; export const swimlaneExternalServiceFormatter: ExternalServiceFormatter = { From 2a3ccf2288dbe509baadde8adab59905a25a1291 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 26 May 2021 15:16:08 +0300 Subject: [PATCH 32/96] Delete comments from incident --- .../actions/server/builtin_action_types/swimlane/schema.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts index 0a9444ca6dde6..6be78c438ee95 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -47,7 +47,6 @@ const SwimlaneFields = { alertSource: schema.nullable(schema.string()), caseId: schema.nullable(schema.string()), caseName: schema.nullable(schema.string()), - comments: schema.nullable(schema.string()), severity: schema.nullable(schema.string()), description: schema.nullable(schema.string()), }; From 4cd8a463fe92b9aedf4fbb412326a10f0611ea16 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 27 May 2021 13:20:23 +0300 Subject: [PATCH 33/96] Clear fields if needed --- .../swimlane/steps/swimlane_fields.tsx | 105 +++++++++++------- 1 file changed, 63 insertions(+), 42 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index 732b359de15a8..28d6da5ffb481 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -9,20 +9,18 @@ import React, { useMemo, useCallback } from 'react'; import { EuiButton, EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import * as i18n from '../translations'; import { StepProps } from './'; +import { SwimlaneFieldMappingConfig, SwimlaneMappingConfig } from '../types'; const SINGLE_SELECTION = { asPlainText: true }; -const empty = { label: '', value: '' }; +const EMPTY_COMBO_BOX_ARRAY: Array> | undefined = []; -const findOption = ( - options: Array>, - searchValue: string -): EuiComboBoxOptionOption => { - return options.find((f) => searchValue === f.value) ?? empty; -}; +const formatOption = (field: SwimlaneFieldMappingConfig) => ({ + label: `${field.name} (${field.key})`, + value: field.id, +}); -const findItem = (fields: StepProps['fields'], searchValue: string | number) => { - return fields.find((f) => searchValue === f.id); -}; +const createSelectedOption = (field: SwimlaneFieldMappingConfig | null | undefined) => + field != null ? [formatOption(field)] : EMPTY_COMBO_BOX_ARRAY; export const SwimlaneFields: React.FunctionComponent = ({ action, @@ -31,26 +29,42 @@ export const SwimlaneFields: React.FunctionComponent = ({ fields, }) => { const { mappings } = action.config; - const options = useMemo( + const [fieldTypeMap, fieldIdMap] = useMemo( () => - fields - .filter((f) => f.fieldType === 'text' || f.fieldType === 'comments') - .map((f) => ({ label: `${f.name} (${f.key})`, value: f.id })) - .sort((a, b) => (a.label?.toLowerCase() > b.label?.toLowerCase() ? 1 : -1)), + fields.reduce( + ([typeMap, idMap], field) => { + if (field != null) { + typeMap.set(field.fieldType, [ + ...(typeMap.get(field.fieldType) ?? []), + formatOption(field), + ]); + idMap.set(field.id, field); + } + + return [typeMap, idMap]; + }, + [ + new Map>>(), + new Map(), + ] + ), [fields] ); + const textOptions = useMemo(() => fieldTypeMap.get('text') ?? [], [fieldTypeMap]); + const commentsOptions = useMemo(() => fieldTypeMap.get('comments') ?? [], [fieldTypeMap]); + const state = useMemo( () => ({ - alertSourceConfig: findOption(options, mappings?.alertSourceConfig?.id), - severityConfig: findOption(options, mappings?.severityConfig?.id), - alertNameConfig: findOption(options, mappings?.alertNameConfig?.id), - caseIdConfig: findOption(options, mappings?.caseIdConfig?.id), - caseNameConfig: findOption(options, mappings?.caseNameConfig?.id), - commentsConfig: findOption(options, mappings?.commentsConfig?.id), - descriptionConfig: findOption(options, mappings?.descriptionConfig?.id), + alertSourceConfig: createSelectedOption(mappings?.alertSourceConfig), + severityConfig: createSelectedOption(mappings?.severityConfig), + alertNameConfig: createSelectedOption(mappings?.alertNameConfig), + caseIdConfig: createSelectedOption(mappings?.caseIdConfig), + caseNameConfig: createSelectedOption(mappings?.caseNameConfig), + commentsConfig: createSelectedOption(mappings?.commentsConfig), + descriptionConfig: createSelectedOption(mappings?.descriptionConfig), }), - [options, mappings] + [mappings] ); const resetConnection = useCallback(() => { @@ -58,30 +72,37 @@ export const SwimlaneFields: React.FunctionComponent = ({ }, [updateCurrentStep]); const editMappings = useCallback( - (key: string, e: Array>) => { - const option = e[0]; - if (!option?.value) { + (key: keyof SwimlaneMappingConfig, e: Array>) => { + if (e.length === 0) { + const newProps = { + ...mappings, + [key]: null, + }; + editActionConfig('mappings', newProps); return; } - const item = findItem(fields, option.value); + + const option = e[0]; + const item = fieldIdMap.get(option.value ?? ''); if (!item) { return; } + const newProps = { ...mappings, [key]: { id: item.id, name: item.name, key: item.key, fieldType: item.fieldType }, }; editActionConfig('mappings', newProps); }, - [editActionConfig, fields, mappings] + [editActionConfig, fieldIdMap, mappings] ); return ( <> editMappings('alertSourceConfig', e)} @@ -90,8 +111,8 @@ export const SwimlaneFields: React.FunctionComponent = ({ editMappings('severityConfig', e)} @@ -100,8 +121,8 @@ export const SwimlaneFields: React.FunctionComponent = ({ editMappings('alertNameConfig', e)} @@ -110,8 +131,8 @@ export const SwimlaneFields: React.FunctionComponent = ({ editMappings('caseIdConfig', e)} @@ -120,8 +141,8 @@ export const SwimlaneFields: React.FunctionComponent = ({ editMappings('caseNameConfig', e)} @@ -130,8 +151,8 @@ export const SwimlaneFields: React.FunctionComponent = ({ editMappings('commentsConfig', e)} @@ -140,8 +161,8 @@ export const SwimlaneFields: React.FunctionComponent = ({ editMappings('descriptionConfig', e)} From 9c28c130b529511c0d5b3c3061393be4ac5218fd Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 27 May 2021 13:33:51 +0300 Subject: [PATCH 34/96] Change reset to configure API --- .../builtin_action_types/swimlane/steps/swimlane_fields.tsx | 4 +--- .../builtin_action_types/swimlane/swimlane_connectors.tsx | 4 ++-- .../builtin_action_types/swimlane/translations.ts | 6 +++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index 28d6da5ffb481..2d78a549da23e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -168,9 +168,7 @@ export const SwimlaneFields: React.FunctionComponent = ({ onChange={(e) => editMappings('descriptionConfig', e)} /> - - {i18n.SW_RETRIEVE_CONFIGURATION_RESET_LABEL} - + {i18n.SW_CONFIGURE_API_LABEL} ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx index 9cb2857780e37..256816837effd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx @@ -41,10 +41,10 @@ const SwimlaneActionConnectorFields: React.FunctionComponent< } else if (step === 1) { setConnectionStatus('incomplete'); setFieldsConfigured('incomplete'); - editActionConfig('mappings', {}); + editActionConfig('mappings', action.config.mappings); } }, - [editActionConfig] + [action.config.mappings, editActionConfig] ); const setupSteps = useMemo( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts index 0d2160e678b5b..a88e982667427 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts @@ -183,7 +183,7 @@ export const SW_RETRIEVE_CONFIGURATION_LABEL = i18n.translate( { defaultMessage: 'Configure Fields' } ); -export const SW_RETRIEVE_CONFIGURATION_RESET_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.resetConfigurationLabel', - { defaultMessage: 'Reset Configuration' } +export const SW_CONFIGURE_API_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.configureAPILabel', + { defaultMessage: 'Configure API' } ); From dd1a32d9df369863efa69795a526fe7da7b874f7 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 27 May 2021 13:59:03 +0300 Subject: [PATCH 35/96] Improve components --- .../swimlane/steps/index.ts | 14 ------ .../swimlane/steps/swimlane_connection.tsx | 21 ++++++-- .../swimlane/steps/swimlane_fields.tsx | 20 ++++++-- .../swimlane/swimlane_connectors.tsx | 48 +++++++++---------- 4 files changed, 56 insertions(+), 47 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/index.ts index 9c8ee334f2143..ca7c39bf1378c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/index.ts @@ -5,19 +5,5 @@ * 2.0. */ -import { SwimlaneConfig, SwimlaneFieldMappingConfig, SwimlaneSecrets } from '../types'; -import { IErrorObject, UserConfiguredActionConnector } from '../../../../../types'; - -export interface StepProps { - action: UserConfiguredActionConnector; - editActionConfig: (property: string, value: any) => void; - editActionSecrets: (property: string, value: any) => void; - errors: IErrorObject; - readOnly: boolean; - updateCurrentStep: (step: number) => void; - updateFields: (items: SwimlaneFieldMappingConfig[]) => void; - fields: SwimlaneFieldMappingConfig[]; -} - export { SwimlaneConnection } from './swimlane_connection'; export { SwimlaneFields } from './swimlane_fields'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx index 90878f54c1a89..51243fc5c2891 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx @@ -13,14 +13,25 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback } from 'react'; import { FormattedMessage } from 'react-intl'; import * as i18n from '../translations'; -import { StepProps } from './'; import { useKibana } from '../../../../../common/lib/kibana'; import { useGetApplication } from '../use_get_application'; +import { SwimlaneConfig, SwimlaneFieldMappingConfig, SwimlaneSecrets } from '../types'; +import { IErrorObject, UserConfiguredActionConnector } from '../../../../../types'; -export const SwimlaneConnection: React.FunctionComponent = ({ +interface Props { + action: UserConfiguredActionConnector; + editActionConfig: (property: string, value: any) => void; + editActionSecrets: (property: string, value: any) => void; + errors: IErrorObject; + readOnly: boolean; + updateCurrentStep: (step: number) => void; + updateFields: (items: SwimlaneFieldMappingConfig[]) => void; +} + +const SwimlaneConnectionComponent: React.FunctionComponent = ({ action, editActionConfig, editActionSecrets, @@ -41,7 +52,7 @@ export const SwimlaneConnection: React.FunctionComponent = ({ appId, apiUrl, }); - const isValid = useMemo(() => apiUrl && apiToken && appId, [apiToken, apiUrl, appId]); + const isValid = apiUrl && apiToken && appId; const connectSwimlane = useCallback(async () => { // fetch swimlane application configuration @@ -166,3 +177,5 @@ export const SwimlaneConnection: React.FunctionComponent = ({ ); }; + +export const SwimlaneConnection = React.memo(SwimlaneConnectionComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index 2d78a549da23e..2bdd51903b33d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -8,8 +8,13 @@ import React, { useMemo, useCallback } from 'react'; import { EuiButton, EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import * as i18n from '../translations'; -import { StepProps } from './'; -import { SwimlaneFieldMappingConfig, SwimlaneMappingConfig } from '../types'; +import { UserConfiguredActionConnector } from '../../../../../types'; +import { + SwimlaneFieldMappingConfig, + SwimlaneMappingConfig, + SwimlaneConfig, + SwimlaneSecrets, +} from '../types'; const SINGLE_SELECTION = { asPlainText: true }; const EMPTY_COMBO_BOX_ARRAY: Array> | undefined = []; @@ -22,7 +27,14 @@ const formatOption = (field: SwimlaneFieldMappingConfig) => ({ const createSelectedOption = (field: SwimlaneFieldMappingConfig | null | undefined) => field != null ? [formatOption(field)] : EMPTY_COMBO_BOX_ARRAY; -export const SwimlaneFields: React.FunctionComponent = ({ +interface Props { + action: UserConfiguredActionConnector; + editActionConfig: (property: string, value: any) => void; + updateCurrentStep: (step: number) => void; + fields: SwimlaneFieldMappingConfig[]; +} + +const SwimlaneFieldsComponent: React.FC = ({ action, editActionConfig, updateCurrentStep, @@ -172,3 +184,5 @@ export const SwimlaneFields: React.FunctionComponent = ({ ); }; + +export const SwimlaneFields = React.memo(SwimlaneFieldsComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx index 256816837effd..aa2b98739aea2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx @@ -16,21 +16,8 @@ const SwimlaneActionConnectorFields: React.FunctionComponent< ActionConnectorFieldsProps > = ({ errors, action, editActionConfig, editActionSecrets, readOnly }) => { const [currentStep, setCurrentStep] = useState(1); - - const stepMap = useMemo<{ [key: number]: any }>( - () => ({ - 1: SwimlaneConnection, - 2: SwimlaneFields, - }), - [] - ); - - const CurrentStepForm = useMemo(() => { - return stepMap[currentStep]; - }, [currentStep, stepMap]); const [connectionStatus, setConnectionStatus] = useState('incomplete'); const [fieldsConfigured, setFieldsConfigured] = useState('incomplete'); - const [fields, setFields] = useState([]); const updateCurrentStep = useCallback( @@ -52,16 +39,16 @@ const SwimlaneActionConnectorFields: React.FunctionComponent< { title: i18n.SW_CONFIGURE_CONNECTION_LABEL, status: connectionStatus, - onClick: () => {}, + onClick: () => updateCurrentStep(1), }, { title: i18n.SW_MAPPING_TITLE_TEXT_FIELD_LABEL, disabled: connectionStatus !== 'complete', status: fieldsConfigured, - onClick: () => {}, + onClick: () => updateCurrentStep(2), }, ], - [connectionStatus, fieldsConfigured] + [connectionStatus, fieldsConfigured, updateCurrentStep] ); const editActionConfigCb = useCallback( @@ -80,16 +67,25 @@ const SwimlaneActionConnectorFields: React.FunctionComponent< - + {currentStep === 1 && ( + + )} + {currentStep === 2 && ( + + )} ); From d64875e9ab0387d499444e7d8b5101190a3ed02f Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 27 May 2021 14:52:28 +0300 Subject: [PATCH 36/96] Lazy load svg icon --- .../builtin_action_types/swimlane/logo.tsx | 53 +++++++++++++++++++ .../swimlane/swimlane.svg | 8 --- .../swimlane/swimlane.tsx | 3 +- 3 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/logo.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.svg diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/logo.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/logo.tsx new file mode 100644 index 0000000000000..d22ff809fe74d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/logo.tsx @@ -0,0 +1,53 @@ +/* + * 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 React from 'react'; + +const Logo = () => { + return ( + + + + + + + + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { Logo as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.svg b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.svg deleted file mode 100644 index b683e68a8ad87..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx index 3e5b20c3d184a..2352039602ae3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx @@ -17,7 +17,6 @@ import { SwimlaneSecrets, SwimlaneActionParams, } from './types'; -import swimlaneSvg from './swimlane.svg'; import * as i18n from './translations'; import { isValidUrl } from '../../../lib/value_validators'; @@ -28,7 +27,7 @@ export function getActionType(): ActionTypeModel< > { return { id: '.swimlane', - iconClass: swimlaneSvg, + iconClass: lazy(() => import('./logo')), selectMessage: i18n.SW_SELECT_MESSAGE_TEXT, actionTypeTitle: i18n.SW_ACTION_TYPE_TITLE, validateConnector: ( From bf6bdb6fad9a86eae5f681071b0b0898125a4d62 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 27 May 2021 17:44:22 +0300 Subject: [PATCH 37/96] Improve step status --- .../builtin_action_types/swimlane/api.ts | 2 +- .../swimlane/swimlane_connectors.tsx | 30 +++++++++++-------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts index 1a0f93ef96197..c6f9d4bee3e13 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts @@ -47,7 +47,7 @@ export async function getApplication({ }); /** - * Fetch does not throws when there is an HTTP error (status >= 400). + * Fetch do not throw when there is an HTTP error (status >= 400). * We need to do it manually. */ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx index aa2b98739aea2..98b9855ebb30e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx @@ -16,18 +16,22 @@ const SwimlaneActionConnectorFields: React.FunctionComponent< ActionConnectorFieldsProps > = ({ errors, action, editActionConfig, editActionSecrets, readOnly }) => { const [currentStep, setCurrentStep] = useState(1); - const [connectionStatus, setConnectionStatus] = useState('incomplete'); - const [fieldsConfigured, setFieldsConfigured] = useState('incomplete'); + const [stepsStatuses, setStepsStatuses] = useState<{ + connection: EuiStepStatus; + fields: EuiStepStatus; + }>({ connection: 'incomplete', fields: 'incomplete' }); const [fields, setFields] = useState([]); const updateCurrentStep = useCallback( (step: number) => { setCurrentStep(step); if (step === 2) { - setConnectionStatus('complete'); + setStepsStatuses((statuses) => ({ ...statuses, connection: 'complete' })); } else if (step === 1) { - setConnectionStatus('incomplete'); - setFieldsConfigured('incomplete'); + setStepsStatuses({ + fields: 'incomplete', + connection: 'incomplete', + }); editActionConfig('mappings', action.config.mappings); } }, @@ -38,29 +42,29 @@ const SwimlaneActionConnectorFields: React.FunctionComponent< () => [ { title: i18n.SW_CONFIGURE_CONNECTION_LABEL, - status: connectionStatus, + status: stepsStatuses.connection, onClick: () => updateCurrentStep(1), }, { title: i18n.SW_MAPPING_TITLE_TEXT_FIELD_LABEL, - disabled: connectionStatus !== 'complete', - status: fieldsConfigured, + disabled: stepsStatuses.connection !== 'complete', + status: stepsStatuses.fields, onClick: () => updateCurrentStep(2), }, ], - [connectionStatus, fieldsConfigured, updateCurrentStep] + [stepsStatuses.connection, stepsStatuses.fields, updateCurrentStep] ); const editActionConfigCb = useCallback( (k: string, v: string) => { editActionConfig(k, v); if (k === 'mappings' && Object.keys(v).length === 6) { - setFieldsConfigured('complete'); - } else if (fieldsConfigured === 'complete') { - setFieldsConfigured('incomplete'); + setStepsStatuses((statuses) => ({ ...statuses, fields: 'complete' })); + } else if (stepsStatuses.fields === 'complete') { + setStepsStatuses((statuses) => ({ ...statuses, fields: 'incomplete' })); } }, - [editActionConfig, fieldsConfigured] + [editActionConfig, stepsStatuses.fields] ); return ( From 5f03f756c2202a5762c5073dd45a0504b3b6f61b Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 27 May 2021 18:57:46 +0300 Subject: [PATCH 38/96] Fix params, api, & schema --- .../builtin_action_types/swimlane/api.ts | 17 +- .../builtin_action_types/swimlane/schema.ts | 6 - .../builtin_action_types/swimlane/types.ts | 15 +- .../builtin_action_types/jira/jira_params.tsx | 6 + .../resilient/resilient_params.tsx | 1 + .../swimlane/steps/swimlane_connection.tsx | 6 +- .../swimlane/steps/swimlane_fields.tsx | 6 +- .../swimlane/swimlane.tsx | 5 +- .../swimlane/swimlane_params.tsx | 247 ++++++++++-------- .../builtin_action_types/swimlane/types.ts | 4 +- 10 files changed, 165 insertions(+), 148 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts index ec6850bf1e786..5a28b938ecd65 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts @@ -6,30 +6,24 @@ */ import { - CreateRecordApiHandlerArgs, ExternalServiceIncidentResponse, ExternalServiceApi, Incident, PushToServiceApiHandlerArgs, } from './types'; -const createRecordHandler = async ({ - externalService, - params, -}: CreateRecordApiHandlerArgs): Promise => { - return await externalService.createRecord({ incident: { ...params, externalId: null } }); -}; - const pushToServiceHandler = async ({ externalService, params, }: PushToServiceApiHandlerArgs): Promise => { const { comments } = params; let res: ExternalServiceIncidentResponse; - const incident: Incident = params.incident; - if (incident.externalId != null) { + const { externalId, ...rest } = params.incident; + const incident: Incident = rest; + + if (externalId != null) { res = await externalService.updateRecord({ - incidentId: incident.externalId, + incidentId: externalId, incident, }); } else { @@ -52,6 +46,5 @@ const pushToServiceHandler = async ({ }; export const api: ExternalServiceApi = { - createRecord: createRecordHandler, pushToService: pushToServiceHandler, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts index 6be78c438ee95..6d6f1fd644441 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -51,8 +51,6 @@ const SwimlaneFields = { description: schema.nullable(schema.string()), }; -export const ExecutorSubActionCreateRecordParamsSchema = schema.object(SwimlaneFields); - export const ExecutorSubActionPushParamsSchema = schema.object({ incident: schema.object({ ...SwimlaneFields, @@ -69,10 +67,6 @@ export const ExecutorSubActionPushParamsSchema = schema.object({ }); export const ExecutorParamsSchema = schema.oneOf([ - schema.object({ - subAction: schema.literal('createRecord'), - subActionParams: ExecutorSubActionCreateRecordParamsSchema, - }), schema.object({ subAction: schema.literal('pushToService'), subActionParams: ExecutorSubActionPushParamsSchema, diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts index 92cd121dc4717..a741651dda3d1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts @@ -11,7 +11,6 @@ import { Logger } from '@kbn/logging'; import { ConfigMappingSchema, ExecutorParamsSchema, - ExecutorSubActionCreateRecordParamsSchema, ExecutorSubActionPushParamsSchema, SwimlaneSecretsConfigurationSchema, SwimlaneServiceConfigurationSchema, @@ -27,10 +26,6 @@ export type MappingConfigType = TypeOf & export type ExecutorParams = TypeOf; export type ExecutorSubActionPushParams = TypeOf; -export type ExecutorSubActionCreateRecordParams = TypeOf< - typeof ExecutorSubActionCreateRecordParamsSchema ->; - export interface ExternalServiceCredentials { config: SwimlanePublicConfigurationType; secrets: SwimlaneSecretConfigurationType; @@ -83,25 +78,17 @@ export interface ExternalService { updateRecord: (params: UpdateRecordParams) => Promise; } -export type Incident = ExecutorSubActionPushParams['incident']; -export type CreateRecordApiParams = ExecutorSubActionCreateRecordParams; +export type Incident = Omit; export interface ExternalServiceApiHandlerArgs { externalService: ExternalService; } -export interface CreateRecordApiHandlerArgs extends ExternalServiceApiHandlerArgs { - params: CreateRecordApiParams; - externalService: ExternalService; - logger: Logger; -} - export interface GetApplicationHandlerArgs { externalService: ExternalService; } export interface ExternalServiceApi { - createRecord: (args: CreateRecordApiHandlerArgs) => Promise; pushToService: (args: PushToServiceApiHandlerArgs) => Promise; } 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 11123a81440bb..8d66325552b8a 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 @@ -63,6 +63,7 @@ const JiraParamsFields: React.FunctionComponent { if (key === 'issueType') { @@ -75,9 +76,11 @@ const JiraParamsFields: React.FunctionComponent { if (incident.issueType != null && fields != null) { const priorities = fields.priority != null ? fields.priority.allowedValues : []; @@ -141,6 +145,7 @@ const JiraParamsFields: React.FunctionComponent { if (!hasPriority && incident.priority != null) { editSubActionProperty('priority', null); @@ -167,6 +172,7 @@ const JiraParamsFields: React.FunctionComponent { if (!actionParams.subAction) { editAction('subAction', 'pushToService', index); 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 4642226d40222..55cdb3b940264 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 @@ -147,6 +147,7 @@ const ResilientParamsFields: React.FunctionComponent { if (!actionParams.subAction) { editAction('subAction', 'pushToService', index); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx index 51243fc5c2891..bf45feed93312 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx @@ -18,11 +18,11 @@ import { FormattedMessage } from 'react-intl'; import * as i18n from '../translations'; import { useKibana } from '../../../../../common/lib/kibana'; import { useGetApplication } from '../use_get_application'; -import { SwimlaneConfig, SwimlaneFieldMappingConfig, SwimlaneSecrets } from '../types'; -import { IErrorObject, UserConfiguredActionConnector } from '../../../../../types'; +import { SwimlaneActionConnector, SwimlaneFieldMappingConfig } from '../types'; +import { IErrorObject } from '../../../../../types'; interface Props { - action: UserConfiguredActionConnector; + action: SwimlaneActionConnector; editActionConfig: (property: string, value: any) => void; editActionSecrets: (property: string, value: any) => void; errors: IErrorObject; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index 2bdd51903b33d..597ecf3a1774c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -8,12 +8,10 @@ import React, { useMemo, useCallback } from 'react'; import { EuiButton, EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import * as i18n from '../translations'; -import { UserConfiguredActionConnector } from '../../../../../types'; import { + SwimlaneActionConnector, SwimlaneFieldMappingConfig, SwimlaneMappingConfig, - SwimlaneConfig, - SwimlaneSecrets, } from '../types'; const SINGLE_SELECTION = { asPlainText: true }; @@ -28,7 +26,7 @@ const createSelectedOption = (field: SwimlaneFieldMappingConfig | null | undefin field != null ? [formatOption(field)] : EMPTY_COMBO_BOX_ARRAY; interface Props { - action: UserConfiguredActionConnector; + action: SwimlaneActionConnector; editActionConfig: (property: string, value: any) => void; updateCurrentStep: (step: number) => void; fields: SwimlaneFieldMappingConfig[]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx index 2352039602ae3..75d073521523c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx @@ -77,7 +77,10 @@ export function getActionType(): ActionTypeModel< comments: new Array(), }; validationResult.errors = errors; - if (!actionParams.subActionParams || !actionParams.subActionParams.alertName?.length) { + if ( + !actionParams.subActionParams || + !actionParams.subActionParams.incident.alertName?.length + ) { errors.alertName.push(i18n.SW_REQUIRED_ALERT_NAME); } return validationResult; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx index aa9da6e96fd8b..fa19c5b32afcc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import React, { useCallback, useEffect, useRef } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import React, { useCallback, useEffect, useRef, useMemo } from 'react'; +import { EuiFormRow, EuiSpacer } from '@elastic/eui'; import * as i18n from './translations'; import { ActionParamsProps } from '../../../../types'; -import { SwimlaneActionParams } from './types'; +import { SwimlaneActionConnector, SwimlaneActionParams } from './types'; import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; @@ -19,74 +19,144 @@ const SwimlaneParamsFields: React.FunctionComponent { - const isInit = useRef(true); - const { - subActionParams = { - alertName: '', - severity: '', - alertSource: '', - caseName: '', - caseId: '', - comments: '', - }, - } = actionParams; + const { incident, comments } = useMemo( + () => + actionParams.subActionParams ?? + (({ + incident: {}, + comments: [], + } as unknown) as SwimlaneActionParams['subActionParams']), + [actionParams.subActionParams] + ); + + const actionConnectorRef = useRef(actionConnector?.id ?? ''); + + const { mappings } = ((actionConnector as unknown) as SwimlaneActionConnector).config; + const { hasAlertName, hasAlertSource, hasComments, hasSeverity } = useMemo( + () => ({ + hasAlertName: mappings.alertNameConfig != null, + hasAlertSource: mappings.alertSourceConfig != null, + hasComments: mappings.commentsConfig != null, + hasSeverity: mappings.severityConfig != null, + }), + [ + mappings.alertNameConfig, + mappings.alertSourceConfig, + mappings.commentsConfig, + mappings.severityConfig, + ] + ); const editSubActionProperty = useCallback( (key: string, value: any) => { - const newProps = { ...subActionParams, [key]: value }; - editAction('subActionParams', newProps, index); + if (key === 'comments') { + return editAction('subActionParams', { incident, comments: value }, index); + } + + return editAction( + 'subActionParams', + { + incident: { ...incident, [key]: value }, + comments, + }, + index + ); + }, + [editAction, incident, comments, index] + ); + + const editComment = useCallback( + (key, value) => { + if (value.length > 0) { + editSubActionProperty(key, [{ commentId: '1', comment: value }]); + } }, - [subActionParams, editAction, index] + [editSubActionProperty] ); useEffect(() => { - if (isInit.current) { - isInit.current = false; - editAction('subAction', 'createRecord', index); + if (actionConnector != null && actionConnectorRef.current !== actionConnector.id) { + actionConnectorRef.current = actionConnector.id; + editAction( + 'subActionParams', + { + incident: {}, + comments: [], + }, + index + ); } - }, [index, editAction]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionConnector]); + + useEffect(() => { + if (!actionParams.subAction) { + editAction('subAction', 'pushToService', index); + } + + if (!actionParams.subActionParams) { + editAction( + 'subActionParams', + { + incident: {}, + comments: [], + }, + index + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionParams]); return ( <> - 0 && subActionParams?.alertName !== undefined} - label={i18n.SW_ALERT_NAME_FIELD_LABEL} - > - - - - 0 && subActionParams?.alertSource !== undefined} - label={i18n.SW_ALERT_SOURCE_FIELD_LABEL} - > - - - - - + {hasAlertName && ( + <> + 0 && incident.alertName !== undefined} + label={i18n.SW_ALERT_NAME_FIELD_LABEL} + > + + + + + )} + {hasAlertSource && ( + <> + 0 && incident.alertSource !== undefined} + label={i18n.SW_ALERT_SOURCE_FIELD_LABEL} + > + + + + + )} + {hasSeverity && ( + <> - - - - 0 && subActionParams?.caseId !== undefined} - label={i18n.SW_CASE_ID_FIELD_LABEL} - > - - - - 0 && subActionParams?.caseName !== undefined} - label={i18n.SW_CASE_NAME_FIELD_LABEL} - > - + + )} + {hasComments && ( + 0 ? comments[0].comment : undefined} + label={i18n.SW_COMMENTS_FIELD_LABEL} /> - - - + )} ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts index 254c616e969f2..7d72582deeb64 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts @@ -7,7 +7,7 @@ import { UserConfiguredActionConnector } from '../../../../types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ExecutorSubActionCreateRecordParams } from '../../../../../../actions/server/builtin_action_types/swimlane/types'; +import { ExecutorSubActionPushParams } from '../../../../../../actions/server/builtin_action_types/swimlane/types'; export type SwimlaneActionConnector = UserConfiguredActionConnector< SwimlaneConfig, @@ -43,7 +43,7 @@ export interface SwimlaneSecrets { export interface SwimlaneActionParams { subAction: string; - subActionParams: ExecutorSubActionCreateRecordParams; + subActionParams: ExecutorSubActionPushParams; } export interface SwimlaneFieldMap { From ba8c671e9322e38fe0692c9cfb55f6fdb470fb0d Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 31 May 2021 19:44:40 +0300 Subject: [PATCH 39/96] Create connector types --- .../builtin_action_types/swimlane/schema.ts | 1 + .../builtin_action_types/swimlane/helpers.ts | 51 ++++ .../swimlane/steps/swimlane_fields.tsx | 248 +++++++++++++----- .../swimlane/swimlane.tsx | 17 +- .../swimlane/swimlane_connectors.tsx | 1 + .../swimlane/translations.ts | 14 + .../builtin_action_types/swimlane/types.ts | 8 + 7 files changed, 267 insertions(+), 73 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts index 6d6f1fd644441..d499739554311 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -31,6 +31,7 @@ export const ConfigMappingSchema = schema.object(ConfigMapping); export const SwimlaneServiceConfiguration = { apiUrl: schema.string(), appId: schema.string(), + connectorType: schema.string(), mappings: ConfigMappingSchema, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts new file mode 100644 index 0000000000000..ebf353d705a18 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SwimlaneConnectorType, SwimlaneMappingConfig } from './types'; +import * as i18n from './translations'; + +const casesRequiredFields = ['caseNameConfig', 'descriptionConfig', 'commentsConfig']; +const casesFields = ['caseIdConfig', ...casesRequiredFields]; +const alertsRequiredFields = ['alertNameConfig']; +const alertsFields = [ + 'alertSourceConfig', + 'severityConfig', + 'commentsConfig', + ...alertsRequiredFields, +]; + +export const isValidFieldForConnector = ( + connector: SwimlaneConnectorType, + field: string +): boolean => { + if (connector === SwimlaneConnectorType.All) { + return true; + } + + return connector === SwimlaneConnectorType.Alerts + ? alertsFields.includes(field) + : casesFields.includes(field); +}; + +export const validateMappingForConnector = ( + connector: SwimlaneConnectorType, + mapping: SwimlaneMappingConfig +): Record => + Object.keys(mapping ?? []).reduce((errors, key) => { + if (connector !== SwimlaneConnectorType.All) { + const isFieldRequired = + connector === SwimlaneConnectorType.Alerts + ? alertsRequiredFields.includes(key) + : casesRequiredFields.includes(key); + + if (isFieldRequired && mapping != null && mapping[key] == null) { + errors = { ...errors, [key]: i18n.SW_FIELD_MAPPING_IS_REQUIRED }; + } + } + + return errors; + }, {} as Record); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index 597ecf3a1774c..547e43263e141 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -6,13 +6,22 @@ */ import React, { useMemo, useCallback } from 'react'; -import { EuiButton, EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { + EuiButton, + EuiFormRow, + EuiComboBox, + EuiComboBoxOptionOption, + EuiRadioGroup, +} from '@elastic/eui'; import * as i18n from '../translations'; import { SwimlaneActionConnector, + SwimlaneConnectorType, SwimlaneFieldMappingConfig, SwimlaneMappingConfig, } from '../types'; +import { IErrorObject } from '../../../../../types'; +import { isValidFieldForConnector } from '../helpers'; const SINGLE_SELECTION = { asPlainText: true }; const EMPTY_COMBO_BOX_ARRAY: Array> | undefined = []; @@ -30,15 +39,23 @@ interface Props { editActionConfig: (property: string, value: any) => void; updateCurrentStep: (step: number) => void; fields: SwimlaneFieldMappingConfig[]; + errors: IErrorObject; } +const radios = [ + { id: 'alerts', label: 'Alerts' }, + { id: 'cases', label: 'Cases' }, + { id: 'all', label: 'All' }, +]; + const SwimlaneFieldsComponent: React.FC = ({ action, editActionConfig, updateCurrentStep, fields, + errors, }) => { - const { mappings } = action.config; + const { mappings, connectorType = SwimlaneConnectorType.All } = action.config; const [fieldTypeMap, fieldIdMap] = useMemo( () => fields.reduce( @@ -77,6 +94,8 @@ const SwimlaneFieldsComponent: React.FC = ({ [mappings] ); + const mappingErrors = useMemo(() => errors?.mappings[0] ?? {}, [errors]); + const resetConnection = useCallback(() => { updateCurrentStep(1); }, [updateCurrentStep]); @@ -106,78 +125,167 @@ const SwimlaneFieldsComponent: React.FC = ({ }, [editActionConfig, fieldIdMap, mappings] ); + return ( <> - - editMappings('alertSourceConfig', e)} - /> - - - editMappings('severityConfig', e)} - /> - - - editMappings('alertNameConfig', e)} - /> - - - editMappings('caseIdConfig', e)} - /> - - - editMappings('caseNameConfig', e)} - /> - - - editMappings('commentsConfig', e)} - /> - - - editMappings('descriptionConfig', e)} + + editActionConfig('connectorType', type)} + name="connectorType" /> + {isValidFieldForConnector( + connectorType as SwimlaneConnectorType.All, + 'alertSourceConfig' + ) && ( + <> + + editMappings('alertSourceConfig', e)} + isInvalid={mappingErrors?.alertSourceConfig != null} + /> + + + )} + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'severityConfig') && ( + <> + + editMappings('severityConfig', e)} + isInvalid={mappingErrors?.severityConfig != null} + /> + + + )} + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'alertNameConfig') && ( + <> + + editMappings('alertNameConfig', e)} + isInvalid={mappingErrors?.alertNameConfig != null} + /> + + + )} + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'caseIdConfig') && ( + <> + + editMappings('caseIdConfig', e)} + isInvalid={mappingErrors?.caseIdConfig != null} + /> + + + )} + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'caseNameConfig') && ( + <> + + editMappings('caseNameConfig', e)} + isInvalid={mappingErrors?.caseNameConfig != null} + /> + + + )} + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'commentsConfig') && ( + <> + + editMappings('commentsConfig', e)} + isInvalid={mappingErrors?.commentsConfig != null} + /> + + + )} + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'descriptionConfig') && ( + <> + + editMappings('descriptionConfig', e)} + isInvalid={mappingErrors?.descriptionConfig != null} + /> + + + )} {i18n.SW_CONFIGURE_API_LABEL} ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx index 75d073521523c..ac1811163e845 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { isEmpty } from 'lodash'; import { lazy } from 'react'; import { ActionTypeModel, @@ -19,6 +20,7 @@ import { } from './types'; import * as i18n from './translations'; import { isValidUrl } from '../../../lib/value_validators'; +import { validateMappingForConnector } from './helpers'; export function getActionType(): ActionTypeModel< SwimlaneConfig, @@ -36,7 +38,8 @@ export function getActionType(): ActionTypeModel< const configErrors = { apiUrl: new Array(), appId: new Array(), - mappings: new Array(), + connectorType: new Array(), + mappings: new Array>(), }; const secretsErrors = { apiToken: new Array(), @@ -58,12 +61,20 @@ export function getActionType(): ActionTypeModel< if (!action.secrets.apiToken) { secretsErrors.apiToken = [...secretsErrors.apiToken, i18n.SW_REQUIRED_API_TOKEN_TEXT]; } + if (!action.config.appId) { configErrors.appId = [...configErrors.appId, i18n.SW_REQUIRED_APP_ID_TEXT]; } - if (!action.config.mappings) { - configErrors.mappings = [...configErrors.mappings, i18n.SW_REQUIRED_FIELD_MAPPINGS_TEXT]; + + const mappingErrors = validateMappingForConnector( + action.config.connectorType, + action.config.mappings + ); + + if (!isEmpty(mappingErrors)) { + configErrors.mappings = [...configErrors.mappings, mappingErrors]; } + return validationResult; }, validateParams: (actionParams: SwimlaneActionParams): GenericValidationResult => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx index 98b9855ebb30e..53c4c80163077 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx @@ -88,6 +88,7 @@ const SwimlaneActionConnectorFields: React.FunctionComponent< editActionConfig={editActionConfigCb} updateCurrentStep={updateCurrentStep} fields={fields} + errors={errors} /> )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts index a88e982667427..78cb8abc9ee46 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts @@ -187,3 +187,17 @@ export const SW_CONFIGURE_API_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.configureAPILabel', { defaultMessage: 'Configure API' } ); + +export const SW_CONNECTOR_TYPE_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.connectorType', + { + defaultMessage: 'Connector Type', + } +); + +export const SW_FIELD_MAPPING_IS_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingFieldRequired', + { + defaultMessage: 'Field mapping is required.', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts index 7d72582deeb64..e81acd84ae223 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts @@ -17,6 +17,7 @@ export type SwimlaneActionConnector = UserConfiguredActionConnector< export interface SwimlaneConfig { apiUrl: string; appId: string; + connectorType: SwimlaneConnectorType; mappings: SwimlaneMappingConfig; } @@ -28,6 +29,7 @@ export interface SwimlaneMappingConfig { alertNameConfig: SwimlaneFieldMappingConfig; commentsConfig: SwimlaneFieldMappingConfig; descriptionConfig: SwimlaneFieldMappingConfig; + [key: string]: SwimlaneFieldMappingConfig; } export interface SwimlaneFieldMappingConfig { @@ -50,3 +52,9 @@ export interface SwimlaneFieldMap { key: string; name: string; } + +export enum SwimlaneConnectorType { + All = 'all', + Alerts = 'alerts', + Cases = 'cases', +} From 851c880ca2816d3daf3c4454232702819c907c16 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 1 Jun 2021 10:54:39 +0300 Subject: [PATCH 40/96] Small fixes --- .../swimlane/steps/swimlane_fields.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index 547e43263e141..01bbebff12f91 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo, useCallback } from 'react'; +import React, { useMemo, useCallback, useEffect } from 'react'; import { EuiButton, EuiFormRow, @@ -94,7 +94,10 @@ const SwimlaneFieldsComponent: React.FC = ({ [mappings] ); - const mappingErrors = useMemo(() => errors?.mappings[0] ?? {}, [errors]); + const mappingErrors: Record = useMemo( + () => (Array.isArray(errors?.mappings) ? errors?.mappings[0] : {}), + [errors] + ); const resetConnection = useCallback(() => { updateCurrentStep(1); @@ -126,6 +129,14 @@ const SwimlaneFieldsComponent: React.FC = ({ [editActionConfig, fieldIdMap, mappings] ); + /** + * Connector type needs to be updated on mount to All. + * Otherwise it is undefined and this will cause an error + * if the user saves the connector without any mapping + */ + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => editActionConfig('connectorType', connectorType), []); + return ( <> From bf1f493222432c7dbe0d464c0989e5d5b3c031a0 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 1 Jun 2021 11:18:22 +0300 Subject: [PATCH 41/96] Show warning when mapping is empty --- .../connectors/swimlane/case_fields.tsx | 38 +++++++++++++++++++ .../components/connectors/swimlane/index.ts | 4 +- .../connectors/swimlane/translations.ts | 14 +++++++ .../builtin_action_types/swimlane/helpers.ts | 2 +- 4 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx new file mode 100644 index 0000000000000..474e3754dc7a3 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx @@ -0,0 +1,38 @@ +/* + * 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 React, { useMemo } from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import * as i18n from './translations'; + +import { SwimlaneFieldsType } from '../../../../common'; +import { ConnectorFieldsProps } from '../types'; + +const casesRequiredFields = ['caseNameConfig', 'descriptionConfig', 'commentsConfig']; +const isMappingEmpty = (mapping: Record | undefined) => + !casesRequiredFields.every((field) => mapping != null && mapping[field] != null); + +const SwimlaneComponent: React.FunctionComponent> = ({ + connector, +}) => { + const { + config: { mappings }, + } = connector; + const showMappingWarning = useMemo(() => isMappingEmpty(mappings), [mappings]); + return ( + <> + {showMappingWarning && ( + + {i18n.EMPTY_MAPPING_WARNING_DESC} + + )} + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SwimlaneComponent as default }; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts index df949151496b2..0503266ba3523 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { lazy } from 'react'; + import { CaseConnector } from '../types'; import { ConnectorTypes, SwimlaneFieldsType } from '../../../../common'; import * as i18n from './translations'; @@ -12,7 +14,7 @@ import * as i18n from './translations'; export const getCaseConnector = (): CaseConnector => { return { id: ConnectorTypes.swimlane, - fieldsComponent: null, + fieldsComponent: lazy(() => import('./case_fields')), }; }; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts index 1b7ea8e834e0e..e132c8f29dba4 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts @@ -25,3 +25,17 @@ export const CASE_NAME_LABEL = i18n.translate('xpack.cases.connectors.swimlane.c export const SEVERITY_LABEL = i18n.translate('xpack.cases.connectors.swimlane.severityLabel', { defaultMessage: 'Severity', }); + +export const EMPTY_MAPPING_WARNING_TITLE = i18n.translate( + 'xpack.cases.connectors.swimlane.emptyMappingWarningTitle', + { + defaultMessage: 'Empty mapping', + } +); + +export const EMPTY_MAPPING_WARNING_DESC = i18n.translate( + 'xpack.cases.connectors.swimlane.emptyMappingWarningDesc', + { + defaultMessage: `The mapping of the connector is empty. Create a connector of type cases before pushing to a case.`, + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts index ebf353d705a18..56650dd8bd8cb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts @@ -35,7 +35,7 @@ export const validateMappingForConnector = ( connector: SwimlaneConnectorType, mapping: SwimlaneMappingConfig ): Record => - Object.keys(mapping ?? []).reduce((errors, key) => { + Object.keys(mapping ?? {}).reduce((errors, key) => { if (connector !== SwimlaneConnectorType.All) { const isFieldRequired = connector === SwimlaneConnectorType.Alerts From ee23c03b0d9c67cce87b4e823d33589164e7cc18 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 1 Jun 2021 11:42:03 +0300 Subject: [PATCH 42/96] Remove case name from fields --- x-pack/plugins/cases/common/api/connectors/swimlane.ts | 1 - x-pack/plugins/cases/server/connectors/case/schema.ts | 1 - .../server/connectors/swimlane/external_service_formatter.ts | 4 ++-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/cases/common/api/connectors/swimlane.ts b/x-pack/plugins/cases/common/api/connectors/swimlane.ts index b1fce5171adf9..2e341b03cefe8 100644 --- a/x-pack/plugins/cases/common/api/connectors/swimlane.ts +++ b/x-pack/plugins/cases/common/api/connectors/swimlane.ts @@ -10,7 +10,6 @@ import * as rt from 'io-ts'; // New fields should also be added at: x-pack/plugins/cases/server/connectors/case/schema.ts export const SwimlaneFieldsRT = rt.type({ caseId: rt.union([rt.string, rt.null]), - caseName: rt.union([rt.string, rt.null]), }); export type SwimlaneFieldsType = rt.TypeOf; diff --git a/x-pack/plugins/cases/server/connectors/case/schema.ts b/x-pack/plugins/cases/server/connectors/case/schema.ts index 11e6587db5f7f..da48faea62962 100644 --- a/x-pack/plugins/cases/server/connectors/case/schema.ts +++ b/x-pack/plugins/cases/server/connectors/case/schema.ts @@ -76,7 +76,6 @@ const ServiceNowSIRFieldsSchema = schema.object({ const SwimlaneFieldsSchema = schema.object({ caseId: schema.nullable(schema.string()), - caseName: schema.nullable(schema.string()), }); const NoneFieldsSchema = schema.nullable(schema.object({})); diff --git a/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts b/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts index cfd923d4f3c31..9d25b8fb663cf 100644 --- a/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts +++ b/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts @@ -9,9 +9,9 @@ import { ExternalServiceFormatter } from '../types'; import { ConnectorSwimlaneTypeFields, SwimlaneFieldsType } from '../../../common'; const format: ExternalServiceFormatter['format'] = (theCase) => { - const { caseId = theCase.id, caseName = theCase.title } = + const { caseId = theCase.id } = (theCase.connector.fields as ConnectorSwimlaneTypeFields['fields']) ?? {}; - return { caseId, caseName }; + return { caseId }; }; export const swimlaneExternalServiceFormatter: ExternalServiceFormatter = { From 9790343499f78a09da6dbc00d1954cd51e4d2731 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 1 Jun 2021 11:45:55 +0300 Subject: [PATCH 43/96] Show connector card --- .../components/connectors/swimlane/case_fields.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx index 474e3754dc7a3..2b8c278e703dd 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx @@ -9,8 +9,9 @@ import React, { useMemo } from 'react'; import { EuiCallOut } from '@elastic/eui'; import * as i18n from './translations'; -import { SwimlaneFieldsType } from '../../../../common'; +import { ConnectorTypes, SwimlaneFieldsType } from '../../../../common'; import { ConnectorFieldsProps } from '../types'; +import { ConnectorCard } from '../card'; const casesRequiredFields = ['caseNameConfig', 'descriptionConfig', 'commentsConfig']; const isMappingEmpty = (mapping: Record | undefined) => @@ -18,6 +19,7 @@ const isMappingEmpty = (mapping: Record | undefined) => const SwimlaneComponent: React.FunctionComponent> = ({ connector, + isEdit = true, }) => { const { config: { mappings }, @@ -25,6 +27,14 @@ const SwimlaneComponent: React.FunctionComponent isMappingEmpty(mappings), [mappings]); return ( <> + {!isEdit && ( + + )} {showMappingWarning && ( {i18n.EMPTY_MAPPING_WARNING_DESC} From a2efccf47b331568020ece88d5472ffd95ec6d05 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 1 Jun 2021 12:03:57 +0300 Subject: [PATCH 44/96] Remove unecessary code --- .../builtin_action_types/swimlane/helpers.ts | 30 +-------- .../builtin_action_types/swimlane/service.ts | 66 ++++++------------- .../builtin_action_types/swimlane/types.ts | 5 ++ 3 files changed, 27 insertions(+), 74 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts index aaa33af3bf534..77d2204cb4d9e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts @@ -7,8 +7,7 @@ import { CreateRecordParams, - ExecutorSubActionCreateRecordParams, - FieldConfig, + ExecutorSubActionPushParams, MappingConfigType, SwimlaneDataComments, SwimlaneDataValues, @@ -38,10 +37,7 @@ export const getBodyForEventAction = ( const createdDate = new Date().toISOString(); const { id, fieldType } = fieldMap; - const paramName = mappingsKey.replace( - 'Config', - '' - ) as keyof ExecutorSubActionCreateRecordParams; + const paramName = mappingsKey.replace('Config', '') as keyof CreateRecordParams['incident']; const value = params[paramName]; @@ -74,25 +70,3 @@ export const getBodyForEventAction = ( return data; }; - -export const removeCommentFieldUpdatedInformation = (content: string): string => { - // these values are added on in `transformFields` in `x-pack/plugins/cases/server/client/cases/utils.ts` - // have to remove to compare string values - // probably a bug - if (content.indexOf(` (updated at `) > 0) { - return content.slice(0, content.indexOf(` (updated at`)); - } - if (content.indexOf(` (created at `) > 0) { - return content.slice(0, content.indexOf(` (created at`)); - } - return content; -}; - -export const removeUnsafeFields = (fields: FieldConfig[]): FieldConfig[] => - fields.filter( - (filter) => - filter.id !== '__proto__' && - filter.key !== '__proto__' && - filter.name !== '__proto__' && - filter.fieldType !== '__proto__' - ); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index 48217a69b9c7c..ee1ea9026ae02 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -10,7 +10,7 @@ import axios from 'axios'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { getErrorMessage, request } from '../lib/axios_utils'; -import { getBodyForEventAction, removeCommentFieldUpdatedInformation } from './helpers'; +import { getBodyForEventAction } from './helpers'; import { CreateCommentParams, CreateRecordParams, @@ -18,7 +18,7 @@ import { ExternalServiceCredentials, ExternalServiceIncidentResponse, MappingConfigType, - SwimlaneComment, + ResponseError, SwimlanePublicConfigurationType, SwimlaneRecordPayload, SwimlaneSecretConfigurationType, @@ -26,6 +26,15 @@ import { } from './types'; import * as i18n from './translations'; +const createErrorMessage = (errorResponse: ResponseError | null | undefined): string => { + if (errorResponse == null) { + return ''; + } + + const { ErrorCode, Argument } = errorResponse; + return `${Argument} (${ErrorCode})`; +}; + export const createExternalService = ( { config, secrets }: ExternalServiceCredentials, logger: Logger, @@ -86,7 +95,9 @@ export const createExternalService = ( throw new Error( getErrorMessage( i18n.NAME, - `Unable to create record in application with id ${appId}. Error: ${error.message}` + `Unable to create record in application with id ${appId}. Error: ${ + error.message + } Reason: ${createErrorMessage(error.response?.data)}` ) ); } @@ -109,47 +120,6 @@ export const createExternalService = ( url: getPostRecordIdUrl(appId, params.incidentId), }); - const fieldId = getCommentFieldId(mappingConfig); - let potentialNewDescription: SwimlaneComment[] = []; - let isDescriptionPosted = true; - - if ( - fieldId != null && - res.data.comments != null && - res.data.comments[fieldId] != null && - res.data.comments[fieldId].length && - data.comments != null && - data.comments[fieldId] != null && - data.comments[fieldId].length === 1 - ) { - // this is the description, it is sent as a comment. - // on update, it needs a separate comment post - // will only ever be length of 1 - potentialNewDescription = data.comments[fieldId]; - // remove update time to compare string only - const messageString = removeCommentFieldUpdatedInformation( - `${potentialNewDescription[0].message}` - ); - - // already saved description/comments - const existingComments: SwimlaneComment[] = res.data.comments[fieldId]; - - // check if description is updated - isDescriptionPosted = existingComments.some( - ({ message }) => removeCommentFieldUpdatedInformation(`${message}`) === messageString - ); - - if (!isDescriptionPosted) { - // if description has updated - // post as comments - await createComment({ - incidentId: params.incidentId, - comment: { comment: potentialNewDescription[0].message }, - createdDate: potentialNewDescription[0].createdDate, - }); - } - } - return { id: res.data.id, title: res.data.name, @@ -159,7 +129,9 @@ export const createExternalService = ( throw new Error( getErrorMessage( i18n.NAME, - `Unable to update record in application with id ${appId}. Error: ${error.message}` + `Unable to update record in application with id ${appId}. Error: ${ + error.message + } Reason: ${createErrorMessage(error.response?.data)}` ) ); } @@ -199,7 +171,9 @@ export const createExternalService = ( throw new Error( getErrorMessage( i18n.NAME, - `Unable to create comment in application with id ${appId}. Error: ${error.message}` + `Unable to create comment in application with id ${appId}. Error: ${ + error.message + } Reason: ${createErrorMessage(error.response?.data)}` ) ); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts index a741651dda3d1..ba346955e12f3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts @@ -112,3 +112,8 @@ export interface CreateCommentParams { comment: SimpleComment; createdDate: string; } + +export interface ResponseError { + ErrorCode: number; + Argument: string; +} From ef4f3b9232de2ff2abf80210d0dadd56e6423d61 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 1 Jun 2021 12:12:48 +0300 Subject: [PATCH 45/96] Show warning message when connector is not of type cases --- x-pack/plugins/cases/common/api/connectors/swimlane.ts | 6 ++++++ .../components/connectors/swimlane/case_fields.tsx | 10 +++++++--- .../components/connectors/swimlane/translations.ts | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/cases/common/api/connectors/swimlane.ts b/x-pack/plugins/cases/common/api/connectors/swimlane.ts index 2e341b03cefe8..bc4d9df9ae6a0 100644 --- a/x-pack/plugins/cases/common/api/connectors/swimlane.ts +++ b/x-pack/plugins/cases/common/api/connectors/swimlane.ts @@ -12,4 +12,10 @@ export const SwimlaneFieldsRT = rt.type({ caseId: rt.union([rt.string, rt.null]), }); +export enum SwimlaneConnectorType { + All = 'all', + Alerts = 'alerts', + Cases = 'cases', +} + export type SwimlaneFieldsType = rt.TypeOf; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx index 2b8c278e703dd..384677d62f603 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx @@ -9,7 +9,7 @@ import React, { useMemo } from 'react'; import { EuiCallOut } from '@elastic/eui'; import * as i18n from './translations'; -import { ConnectorTypes, SwimlaneFieldsType } from '../../../../common'; +import { ConnectorTypes, SwimlaneFieldsType, SwimlaneConnectorType } from '../../../../common'; import { ConnectorFieldsProps } from '../types'; import { ConnectorCard } from '../card'; @@ -22,9 +22,13 @@ const SwimlaneComponent: React.FunctionComponent { const { - config: { mappings }, + config: { mappings, connectorType }, } = connector; - const showMappingWarning = useMemo(() => isMappingEmpty(mappings), [mappings]); + const showMappingWarning = useMemo( + () => connectorType !== SwimlaneConnectorType.Cases || isMappingEmpty(mappings), + [mappings, connectorType] + ); + return ( <> {!isEdit && ( diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts index e132c8f29dba4..0428b58080099 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts @@ -36,6 +36,6 @@ export const EMPTY_MAPPING_WARNING_TITLE = i18n.translate( export const EMPTY_MAPPING_WARNING_DESC = i18n.translate( 'xpack.cases.connectors.swimlane.emptyMappingWarningDesc', { - defaultMessage: `The mapping of the connector is empty. Create a connector of type cases before pushing to a case.`, + defaultMessage: `The mapping of the connector is empty. Create a connector of type Cases before pushing to a case.`, } ); From f28e4f5008c742dc81ce5fd6b1cc3810154e5139 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 1 Jun 2021 12:41:51 +0300 Subject: [PATCH 46/96] Show warning message when connector is not of type alerts --- .../swimlane/swimlane.tsx | 17 +++---- .../swimlane/swimlane_params.tsx | 44 +++++++++++-------- .../swimlane/translations.ts | 14 ++++++ 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx index ac1811163e845..062ea13383dd9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx @@ -78,21 +78,18 @@ export function getActionType(): ActionTypeModel< return validationResult; }, validateParams: (actionParams: SwimlaneActionParams): GenericValidationResult => { - const validationResult = { errors: {} }; const errors = { - alertName: new Array(), - caseId: new Array(), - severity: new Array(), - caseName: new Array(), - alertSource: new Array(), - comments: new Array(), + 'subActionParams.incident.alertName': new Array(), + }; + const validationResult = { + errors, }; - validationResult.errors = errors; if ( - !actionParams.subActionParams || + actionParams.subActionParams && + actionParams.subActionParams.incident && !actionParams.subActionParams.incident.alertName?.length ) { - errors.alertName.push(i18n.SW_REQUIRED_ALERT_NAME); + errors['subActionParams.incident.alertName'].push(i18n.SW_REQUIRED_ALERT_NAME); } return validationResult; }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx index fa19c5b32afcc..1355b2f98cf30 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx @@ -6,10 +6,10 @@ */ import React, { useCallback, useEffect, useRef, useMemo } from 'react'; -import { EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiFormRow, EuiSpacer } from '@elastic/eui'; import * as i18n from './translations'; import { ActionParamsProps } from '../../../../types'; -import { SwimlaneActionConnector, SwimlaneActionParams } from './types'; +import { SwimlaneActionConnector, SwimlaneActionParams, SwimlaneConnectorType } from './types'; import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; @@ -33,7 +33,10 @@ const SwimlaneParamsFields: React.FunctionComponent ({ hasAlertName: mappings.alertNameConfig != null, @@ -49,6 +52,10 @@ const SwimlaneParamsFields: React.FunctionComponent { if (key === 'comments') { @@ -109,15 +116,18 @@ const SwimlaneParamsFields: React.FunctionComponent {hasAlertName && ( <> 0 && incident.alertName !== undefined} + error={errors['subActionParams.incident.alertName'] ?? ''} + isInvalid={ + errors['subActionParams.incident.alertName']?.length > 0 && + incident.alertName !== undefined + } label={i18n.SW_ALERT_NAME_FIELD_LABEL} > @@ -135,21 +145,14 @@ const SwimlaneParamsFields: React.FunctionComponent - 0 && incident.alertSource !== undefined} - label={i18n.SW_ALERT_SOURCE_FIELD_LABEL} - > + @@ -164,8 +167,7 @@ const SwimlaneParamsFields: React.FunctionComponent @@ -183,6 +185,10 @@ const SwimlaneParamsFields: React.FunctionComponent )} + ) : ( + + {i18n.EMPTY_MAPPING_WARNING_DESC} + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts index 78cb8abc9ee46..21b3301c1067a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts @@ -201,3 +201,17 @@ export const SW_FIELD_MAPPING_IS_REQUIRED = i18n.translate( defaultMessage: 'Field mapping is required.', } ); + +export const EMPTY_MAPPING_WARNING_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningTitle', + { + defaultMessage: 'Empty mapping', + } +); + +export const EMPTY_MAPPING_WARNING_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningDesc', + { + defaultMessage: `The mapping of the connector is empty. Create a connector of type Alerts before pushing to a case.`, + } +); From 1a68b0e33ef8d89a9e601516bcf1bb218e8590a8 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 1 Jun 2021 12:57:56 +0300 Subject: [PATCH 47/96] Remove optional from target mapping field --- x-pack/plugins/cases/common/api/connectors/mappings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/common/api/connectors/mappings.ts b/x-pack/plugins/cases/common/api/connectors/mappings.ts index 9933c106befa8..ee38dac3212cc 100644 --- a/x-pack/plugins/cases/common/api/connectors/mappings.ts +++ b/x-pack/plugins/cases/common/api/connectors/mappings.ts @@ -26,7 +26,7 @@ export type ThirdPartyField = rt.TypeOf; export const ConnectorMappingsAttributesRT = rt.type({ action_type: ActionTypeRT, source: CaseFieldRT, - target: rt.union([ThirdPartyFieldRT, rt.undefined]), + target: ThirdPartyFieldRT, }); export const ConnectorMappingsRt = rt.type({ From 79c8bfdb38e1f072f7db2856768944c0f9d2c986 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 1 Jun 2021 13:19:21 +0300 Subject: [PATCH 48/96] Fix types --- .../builtin_action_types/swimlane/api.test.ts | 33 +----------- .../swimlane/helpers.test.ts | 43 ++------------- .../builtin_action_types/swimlane/helpers.ts | 1 - .../builtin_action_types/swimlane/index.ts | 13 ----- .../builtin_action_types/swimlane/mocks.ts | 18 ++++--- .../swimlane/service.test.ts | 53 ++----------------- .../swimlane/swimlane_params.test.tsx | 17 +++--- 7 files changed, 30 insertions(+), 148 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts index 3badb94e6d4d4..679a062fcb9b6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts @@ -7,12 +7,7 @@ import { api } from './api'; import { ExternalService } from './types'; -import { - externalServiceMock, - recordResponseCreate, - recordResponseUpdate, - getApplicationResponse, -} from './mocks'; +import { externalServiceMock, recordResponseCreate, recordResponseUpdate } from './mocks'; import { Logger } from '@kbn/logging'; let mockedLogger: jest.Mocked; @@ -23,6 +18,7 @@ const params = { alertSource: 'elastic', caseId: '123456', comments: 'some comments', + description: 'case desc', }; describe('api', () => { @@ -32,31 +28,6 @@ describe('api', () => { externalService = externalServiceMock.create(); }); - describe('getApplication', () => { - test('it returns the fields correctly', async () => { - const res = await api.getApplication({ externalService }); - expect(res).toEqual(getApplicationResponse); - }); - }); - - describe('createRecord', () => { - test('it creates a record correctly with a comment', async () => { - const res = await api.createRecord({ - externalService, - logger: mockedLogger, - params: { - alertName: 'alert name', - caseName: 'case name', - severity: 'critical', - alertSource: 'elastic', - caseId: '123456', - comments: 'some comments', - }, - }); - expect(res).toEqual(recordResponseCreate); - }); - }); - describe('pushToService', () => { test('it pushes a new record', async () => { const res = await api.pushToService({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts index aed3e406e4e9a..a456a52a8d03e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getBodyForEventAction, removeUnsafeFields } from './helpers'; +import { getBodyForEventAction } from './helpers'; import { mappings } from './mocks'; describe('Create Record Mapping', () => { @@ -19,6 +19,7 @@ describe('Create Record Mapping', () => { caseName: 'Case Name', caseId: 'es3456789', comments: 'This is a comment', + description: 'case desc', externalId: null, }; const data = getBodyForEventAction(appId, mappings, params); @@ -28,44 +29,6 @@ describe('Create Record Mapping', () => { expect(data?.values?.[mappings.caseIdConfig?.id ?? 0]).toEqual(params.caseId); expect(data?.values?.[mappings.commentsConfig?.id ?? 0]).toEqual(params.comments); expect(data?.values?.[mappings?.severityConfig?.id ?? 0]).toEqual(params.severity); - }); -}); - -describe('removeUnsafeFields', () => { - const fields = [ - { - id: '__proto__', - name: 'Alert Source', - key: 'alert-source', - fieldType: 'text', - }, - { - id: 'adnjls', - name: '__proto__', - key: 'alert-source', - fieldType: 'text', - }, - { - id: 'adnjls', - name: 'Alert Source', - key: '__proto__', - fieldType: 'text', - }, - { - id: 'adnjls', - name: 'Alert Source', - key: 'alert-source', - fieldType: '__proto__', - }, - { - id: 'safe', - name: 'safe', - key: 'safe', - fieldType: 'safe', - }, - ]; - test('it returns only safe fields', () => { - const safeFields = removeUnsafeFields(fields); - expect(safeFields).toEqual([fields[4]]); + expect(data?.values?.[mappings?.descriptionConfig?.id ?? 0]).toEqual(params.description); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts index 77d2204cb4d9e..36f9b6b711c93 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts @@ -7,7 +7,6 @@ import { CreateRecordParams, - ExecutorSubActionPushParams, MappingConfigType, SwimlaneDataComments, SwimlaneDataValues, diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts index 5abb7b2e5c5d6..77afe4a0f4f01 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts @@ -16,7 +16,6 @@ import { SwimlanePublicConfigurationType, SwimlaneSecretConfigurationType, ExecutorParams, - ExecutorSubActionCreateRecordParams, ExecutorSubActionPushParams, } from './types'; import { validate } from './validators'; @@ -101,18 +100,6 @@ async function executor( throw new Error(errorMessage); } - if (subAction === 'createRecord') { - const createRecordParams = subActionParams as ExecutorSubActionCreateRecordParams; - - data = await api.createRecord({ - externalService, - params: createRecordParams, - logger, - }); - - logger.debug(`Swimlane new record id ${data.id}`); - } - if (subAction === 'pushToService') { const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts index 7e4f71485a2ea..f1185284f3562 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts @@ -5,11 +5,7 @@ * 2.0. */ -import { - CreateRecordApiParams, - ExecutorSubActionCreateRecordParams, - ExternalService, -} from './types'; +import { ExternalService } from './types'; export const applicationFields = [ { @@ -46,6 +42,12 @@ export const applicationFields = [ id: 'a6fdf', name: 'Comments', key: 'comments', + fieldType: 'notes', + }, + { + id: 'a6fde', + name: 'Description', + key: 'description', fieldType: 'text', }, ]; @@ -57,6 +59,7 @@ export const mappings = { caseIdConfig: applicationFields[3], caseNameConfig: applicationFields[4], commentsConfig: applicationFields[5], + descriptionConfig: applicationFields[6], }; export const getApplicationResponse = { fields: applicationFields }; @@ -79,7 +82,6 @@ export const commentResponse = { const createMock = (): jest.Mocked => { return { - getApplication: jest.fn().mockImplementation(() => Promise.resolve(getApplicationResponse)), createComment: jest.fn().mockImplementation(() => Promise.resolve(commentResponse)), createRecord: jest.fn().mockImplementation(() => Promise.resolve(recordResponseCreate)), updateRecord: jest.fn().mockImplementation(() => Promise.resolve(recordResponseUpdate)), @@ -90,7 +92,7 @@ const externalServiceMock = { create: createMock, }; -const executorParams: ExecutorSubActionCreateRecordParams = { +const executorParams = { alertName: 'alert-name', alertSource: 'alert-source', caseId: 'case-id', @@ -99,7 +101,7 @@ const executorParams: ExecutorSubActionCreateRecordParams = { severity: 'severity', }; -const apiParams: CreateRecordApiParams = { +const apiParams = { ...executorParams, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts index 34c28179b5b9b..4d0a7eb1a0be2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts @@ -12,7 +12,7 @@ import { Logger } from '../../../../../../src/core/server'; import { actionsConfigMock } from '../../actions_config.mock'; import * as utils from '../lib/axios_utils'; import { createExternalService } from './service'; -import { mappings, applicationFields } from './mocks'; +import { mappings } from './mocks'; import { ExternalService } from './types'; const logger = loggingSystemMock.create().get() as jest.Mocked; @@ -35,6 +35,7 @@ describe('Swimlane Service', () => { const config = { apiUrl: 'https://test.swimlane.com/', appId: 'bcq16kdTbz5jlwM6h', + connectorType: 'all', mappings, }; @@ -52,6 +53,7 @@ describe('Swimlane Service', () => { comments: 'Comments', severity: 'Severity', externalId: null, + description: 'case desc', }; beforeAll(() => { @@ -128,7 +130,7 @@ describe('Swimlane Service', () => { expect(() => { return createExternalService( { - config: { apiUrl: 'test.com', appId: '78978', mappings }, + config: { apiUrl: 'test.com', appId: '78978', mappings, connectorType: 'all' }, secrets: { // @ts-ignore apiToken: null, @@ -141,53 +143,6 @@ describe('Swimlane Service', () => { }); }); - describe('getApplication', () => { - test('it returns the fields correctly', async () => { - requestMock.mockImplementation(() => ({ - data: { - fields: [ - ...applicationFields, - { - id: '__proto__', - name: 'unsafe', - key: 'unsafe', - fieldType: 'unsafe', - }, - ], - }, - })); - - const res = await service.getApplication(); - expect(res).toEqual({ fields: applicationFields }); - }); - - test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { fields: [] }, - })); - - await service.getApplication(); - expect(requestMock).toHaveBeenCalledWith({ - axios, - logger, - headers, - url: `${config.apiUrl.slice(0, -1)}/api/app/${config.appId}`, - method: 'get', - configurationUtilities, - }); - }); - - test('it should throw an error', async () => { - requestMock.mockImplementation(() => { - throw new Error('An error has occurred'); - }); - - await expect(service.getApplication()).rejects.toThrow( - `[Action][Swimlane]: Unable to get application with id ${config.appId}. Error: An error has occurred` - ); - }); - }); - describe('createRecord', () => { test('it creates a record correctly', async () => { requestMock.mockImplementation(() => ({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx index 2b61a9c5aa34f..8156762631f50 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx @@ -12,13 +12,18 @@ import SwimlaneParamsFields from './swimlane_params'; describe('SwimlaneParamsFields renders', () => { test('all params fields is rendered', () => { const actionParams = { + subAction: 'pushToService', subActionParams: { - alertName: 'alert name', - alertSource: 'alert source', - caseId: '3456789', - caseName: 'my case name', - comments: 'my comments', - severity: 'critical', + incident: { + alertName: 'alert name', + alertSource: 'alert source', + caseId: '3456789', + caseName: 'my case name', + severity: 'critical', + description: 'case desc', + externalId: null, + }, + comments: [], }, }; From e33403f978dd840d7700b6b4b1ab7e74423593c7 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 1 Jun 2021 13:21:22 +0300 Subject: [PATCH 49/96] Improve order --- .../builtin_action_types/swimlane/steps/swimlane_fields.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index 01bbebff12f91..9560a0fce2a85 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -43,9 +43,9 @@ interface Props { } const radios = [ + { id: 'all', label: 'All' }, { id: 'alerts', label: 'Alerts' }, { id: 'cases', label: 'Cases' }, - { id: 'all', label: 'All' }, ]; const SwimlaneFieldsComponent: React.FC = ({ From a237cb08af29879125e4e423eac13c34788e5835 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 1 Jun 2021 14:28:11 +0300 Subject: [PATCH 50/96] Fix tests --- .../swimlane/service.test.ts | 3 +- .../builtin_action_types/swimlane/mocks.ts | 61 +++++++++++++++++++ .../swimlane/swimlane.test.tsx | 35 ++--------- .../swimlane/swimlane_params.test.tsx | 14 ++++- 4 files changed, 79 insertions(+), 34 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts index 4d0a7eb1a0be2..1f1d292df2f36 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts @@ -53,7 +53,7 @@ describe('Swimlane Service', () => { comments: 'Comments', severity: 'Severity', externalId: null, - description: 'case desc', + description: 'Description', }; beforeAll(() => { @@ -188,6 +188,7 @@ describe('Swimlane Service', () => { [mappings.caseIdConfig.id]: 'Case Id', [mappings.commentsConfig.id]: 'Comments', [mappings.severityConfig.id]: 'Severity', + [mappings.descriptionConfig.id]: 'Description', }, }, url: `${config.apiUrl.slice(0, -1)}/api/app/${config.appId}/record`, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts new file mode 100644 index 0000000000000..efc437221ecae --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts @@ -0,0 +1,61 @@ +/* + * 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. + */ + +export const applicationFields = [ + { + id: 'adnjls', + name: 'Alert Source', + key: 'alert-source', + fieldType: 'text', + }, + { + id: 'adnlas', + name: 'Severity', + key: 'severity', + fieldType: 'text', + }, + { + id: 'adnfls', + name: 'Alert Name', + key: 'alert-name', + fieldType: 'text', + }, + { + id: 'a6sst', + name: 'Case Id', + key: 'case-id-name', + fieldType: 'text', + }, + { + id: 'a6fst', + name: 'Case Name', + key: 'case-name', + fieldType: 'text', + }, + { + id: 'a6fdf', + name: 'Comments', + key: 'comments', + fieldType: 'notes', + }, + { + id: 'a6fde', + name: 'Description', + key: 'description', + fieldType: 'text', + }, +]; + +export const mappings = { + alertSourceConfig: applicationFields[0], + severityConfig: applicationFields[1], + alertNameConfig: applicationFields[2], + caseIdConfig: applicationFields[3], + caseNameConfig: applicationFields[4], + commentsConfig: applicationFields[5], + descriptionConfig: applicationFields[6], +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx index a1de00ae117fc..74ffb2aab2629 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx @@ -25,7 +25,6 @@ beforeAll(() => { describe('actionTypeRegistry.get() works', () => { test('action type static data is as expected', () => { expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); - expect(actionTypeModel.iconClass).toEqual('test-file-stub'); }); }); @@ -47,7 +46,7 @@ describe('swimlane connector validation', () => { }, } as SwimlaneActionConnector; expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { errors: { apiUrl: [], appId: [], mappings: [] } }, + config: { errors: { apiUrl: [], appId: [], mappings: [], connectorType: [] } }, secrets: { errors: { apiToken: [] } }, }); @@ -55,31 +54,10 @@ describe('swimlane connector validation', () => { delete actionConnector.config.apiUrl; actionConnector.secrets.apiToken = 'test1'; expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { errors: { apiUrl: ['URL is required.'], appId: [], mappings: [] } }, - secrets: { errors: { apiToken: [] } }, - }); - }); - - test('connector validation fails when connector config is not valid', () => { - const actionConnector = { - secrets: { - apiToken: 'test', - }, - id: 'test', - actionTypeId: '.swimlane', - name: 'swimlane', config: { - apiUrl: 'http:\\test', - appId: '1234567asbd32', - }, - } as SwimlaneActionConnector; - - expect(actionTypeModel.validateConnector(actionConnector).config).toEqual({ - errors: { - apiUrl: [], - appId: [], - mappings: ['Field mappings are required.'], + errors: { apiUrl: ['URL is required.'], appId: [], mappings: [], connectorType: [] }, }, + secrets: { errors: { apiToken: [] } }, }); }); }); @@ -94,12 +72,7 @@ describe('swimlane action params validation', () => { expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { - alertName: [], - alertSource: [], - caseId: [], - caseName: [], - comments: [], - severity: [], + 'subActionParams.incident.alertName': [], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx index 8156762631f50..c87320a2f356a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx @@ -8,6 +8,8 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import SwimlaneParamsFields from './swimlane_params'; +import { SwimlaneConnectorType } from './types'; +import { mappings } from './mocks'; describe('SwimlaneParamsFields renders', () => { test('all params fields is rendered', () => { @@ -27,6 +29,15 @@ describe('SwimlaneParamsFields renders', () => { }, }; + const connector = { + secrets: {}, + config: { mappings, connectorType: SwimlaneConnectorType.All }, + id: 'test', + actionTypeId: '.test', + name: 'Test', + isPreconfigured: false, + }; + const wrapper = mountWithIntl( { caseName: [], alertSource: [], }} + actionConnector={connector} editAction={() => {}} index={0} /> ); expect(wrapper.find('[data-test-subj="severity"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="caseId"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="caseName"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="comments"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="alertSource"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="alertName"]').length > 0).toBeTruthy(); From fbbd76285abe962e2e05ac69ebe1f59a640b8636 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 1 Jun 2021 20:37:20 +0300 Subject: [PATCH 51/96] Improve responses --- .../builtin_action_types/swimlane/service.ts | 15 ++++++++++++--- .../server/builtin_action_types/swimlane/types.ts | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index ee1ea9026ae02..428f68620b94d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -52,7 +52,6 @@ export const createExternalService = ( const headers: Record = { 'Content-Type': 'application/json', 'Private-Token': `${secrets.apiToken}`, - 'kbn-xsrf': 'true', }; const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url; @@ -90,7 +89,12 @@ export const createExternalService = ( method: 'post', url: getPostRecordUrl(appId), }); - return { id: res.data.id, title: res.data.name, url: getRecordIdUrl(appId, res.data.id) }; + return { + id: res.data.id, + title: res.data.name, + url: getRecordIdUrl(appId, res.data.id), + pushedDate: new Date(res.data.createdDate).toISOString(), + }; } catch (error) { throw new Error( getErrorMessage( @@ -124,6 +128,7 @@ export const createExternalService = ( id: res.data.id, title: res.data.name, url: getRecordIdUrl(appId, params.incidentId), + pushedDate: new Date(res.data.modifiedDate).toISOString(), }; } catch (error) { throw new Error( @@ -163,8 +168,12 @@ export const createExternalService = ( url: getPostCommentUrl(appId, incidentId, fieldId), }); - // TODO: Check if commentId and externalCommentId is needed. + /** + * Swimlane response does not contain any data. + * We cannot get an externalCommentId + */ return { + commentId: comment.commentId, pushedDate: createdDate, }; } catch (error) { diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts index ba346955e12f3..4d896d74e481e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts @@ -53,6 +53,7 @@ export interface ExternalServiceIncidentResponse { id: string; title: string; url: string; + pushedDate: string; } export interface ExternalServiceCommentResponse { pushedDate: string; From 2c72b6df38b51ed0ea80b1d17344c209e9422918 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 2 Jun 2021 09:46:25 +0300 Subject: [PATCH 52/96] Small fixes --- .../swimlane/service.test.ts | 18 +++++++++--------- .../cases/server/client/configure/utils.ts | 2 +- .../actions_simulators/server/README.md | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts index 1f1d292df2f36..f767277fb2b2a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts @@ -42,7 +42,6 @@ describe('Swimlane Service', () => { const headers = { 'Content-Type': 'application/json', 'Private-Token': 'token', - 'kbn-xsrf': 'true', }; const incident = { @@ -144,12 +143,15 @@ describe('Swimlane Service', () => { }); describe('createRecord', () => { + const data = { + id: '123', + name: 'title', + createdDate: '2021-06-01T17:29:51.092Z', + }; + test('it creates a record correctly', async () => { requestMock.mockImplementation(() => ({ - data: { - id: '123', - name: 'title', - }, + data, })); const res = await service.createRecord({ @@ -159,16 +161,14 @@ describe('Swimlane Service', () => { expect(res).toEqual({ id: '123', title: 'title', + pushedDate: '2021-06-01T17:29:51.092Z', url: `${config.apiUrl.slice(0, -1)}/record/${config.appId}/123`, }); }); test('it should call request with correct arguments', async () => { requestMock.mockImplementation(() => ({ - data: { - id: '123', - name: 'title', - }, + data, })); await service.createRecord({ diff --git a/x-pack/plugins/cases/server/client/configure/utils.ts b/x-pack/plugins/cases/server/client/configure/utils.ts index a5b697ac0e6fd..e5db30fb90095 100644 --- a/x-pack/plugins/cases/server/client/configure/utils.ts +++ b/x-pack/plugins/cases/server/client/configure/utils.ts @@ -90,7 +90,7 @@ export const formatFields = (theData: unknown, theType: string): ConnectorField[ } }; -const getPreferredFields = (theType: string, swimlaneMappings?: SwimlaneMappings) => { +const getPreferredFields = (theType: string) => { let title: string = ''; let description: string = ''; let comments: string = ''; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/README.md b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/README.md index 26af24e73c7c1..c248bdce3785d 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/README.md +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/README.md @@ -17,7 +17,7 @@ simulator usage This may get out of date, consult the code for exact urls and inputs. Each simulator's last path segment should be the name of the service (eg, slack, -pagerduty, swimlane, etc). +pagerduty, etc). ```console $ export SLACK_URL=http://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/slack From e9f5a576cc78a631216938f4bc091f56a0dfae42 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 2 Jun 2021 10:33:44 +0300 Subject: [PATCH 53/96] Fix integration tests --- .../builtin_action_types/swimlane/service.ts | 2 +- .../actions/builtin_action_types/swimlane.ts | 2 + .../server/swimlane_simulation.ts | 2 + .../actions/builtin_action_types/swimlane.ts | 58 +++++++++---------- 4 files changed, 31 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index 428f68620b94d..943cceecad735 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -32,7 +32,7 @@ const createErrorMessage = (errorResponse: ResponseError | null | undefined): st } const { ErrorCode, Argument } = errorResponse; - return `${Argument} (${ErrorCode})`; + return Argument != null && ErrorCode != null ? `${Argument} (${ErrorCode})` : ''; }; export const createExternalService = ( diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts index d181f5dbd8b00..bf98861c1e05e 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts @@ -21,6 +21,7 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { config: { apiUrl: 'http://swimlane.mynonexistent.co', appId: '123456asdf', + connectorType: 'all', mappings: { alertSourceConfig: { id: 'adnjls', @@ -64,6 +65,7 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { apiToken: 'swimlane-api-key', }, }; + describe('swimlane', () => { let swimlaneSimulatorURL: string = ''; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.ts index bf673ba843e1c..9bff58f67cace 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.ts @@ -33,6 +33,7 @@ export function initPlugin(router: IRouter, path: string) { return jsonResponse(res, 200, { id: 'wowzeronza', name: 'ET-69', + createdDate: '2021-06-01T17:29:51.092Z', }); } ); @@ -55,6 +56,7 @@ export function initPlugin(router: IRouter, path: string) { return jsonResponse(res, 200, { id: 'wowzeronza', name: 'ET-69', + modifiedDate: '2021-06-01T17:29:51.092Z', }); } ); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts index 455db46a02535..7767ae89f56f4 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts @@ -28,6 +28,7 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { config: { apiUrl: 'http://swimlane.mynonexistent.com', appId: '123456asdf', + connectorType: 'all', mappings: { alertSourceConfig: { id: 'adnjls', @@ -63,6 +64,12 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { id: 'a6fdf', name: 'Comments', key: 'comments', + fieldType: 'notes', + }, + descriptionConfig: { + id: 'a6fdf', + name: 'Description', + key: 'description', fieldType: 'text', }, }, @@ -79,17 +86,19 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { alertSource: 'Elastic', caseName: 'Case Name', caseId: 'es3456789', - comments: 'This is a comment', + description: 'This is a description', externalId: null, }, comments: [ { comment: 'first comment', + commentId: '123', }, ], }, }, }; + describe('Swimlane', () => { let swimlaneSimulatorURL: string = ''; // need to wait for kibanaServer to settle ... @@ -322,43 +331,24 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [createRecord]\n- [1.subAction]: expected value to equal [pushToService]', + 'error validating action params: [subAction]: expected value to equal [pushToService]', }); }); }); + /** + * All subActionParams are optional. + * If subActionParams is not provided all + * the subActionParams attributes will be set to null + * and the validation will succeed. For that reason, + * the subActionParams need to be set to null. + */ it('should handle failing with a simulated success without subActionParams', async () => { await supertest .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ - params: { subAction: 'pushToService' }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - connector_id: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [createRecord]\n- [1.subActionParams.incident.alertName]: expected value of type [string] but got [undefined]', - }); - }); - }); - - it('should handle failing with a simulated success without alertName', async () => { - await supertest - .post(`/api/actions/connector/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockSwimlane.params, - subActionParams: { - incident: { - severity: 'very much so', - }, - comments: [], - }, - }, + params: { subAction: 'pushToService', subActionParams: null }, }) .then((resp: any) => { expect(resp.body).to.eql({ @@ -366,7 +356,7 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [createRecord]\n- [1.subActionParams.incident.alertName]: expected value of type [string] but got [undefined]', + 'error validating action params: [subActionParams]: expected a plain object value, but found [null] instead.', }); }); }); @@ -390,7 +380,7 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [createRecord]\n- [1.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + 'error validating action params: [subActionParams.incident.comments]: definition for this key is missing', }); }); }); @@ -414,11 +404,12 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [createRecord]\n- [1.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + 'error validating action params: [subActionParams.incident.comments]: definition for this key is missing', }); }); }); }); + describe('Execution', () => { it('should handle creating an incident', async () => { const { body } = await supertest @@ -442,10 +433,12 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { data: { id: 'wowzeronza', title: 'ET-69', + pushedDate: '2021-06-01T17:29:51.092Z', url: `${swimlaneSimulatorURL}/record/123456asdf/wowzeronza`, }, }); }); + it('should handle updating an incident', async () => { const { body } = await supertest .post(`/api/actions/connector/${simulatedActionId}/_execute`) @@ -471,6 +464,7 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { data: { id: 'wowzeronza', title: 'ET-69', + pushedDate: '2021-06-01T17:29:51.092Z', url: `${swimlaneSimulatorURL}/record/123456asdf/wowzeronza`, }, }); From b8f2c0d630c8606f5208a4250e5ec8884625f710 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 2 Jun 2021 10:53:14 +0300 Subject: [PATCH 54/96] Update readme --- x-pack/plugins/actions/README.md | 170 +++++++++++++------------------ 1 file changed, 71 insertions(+), 99 deletions(-) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 08c76e744e433..131935213d023 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -1,4 +1,4 @@ -# Kibana Actions +****# Kibana Actions The Kibana actions plugin provides a framework to create executable actions. You can: @@ -14,24 +14,6 @@ The Kibana actions plugin provides a framework to create executable actions. You Table of Contents -- [Kibana Actions](#kibana-actions) - - [Terminology](#terminology) - - [Usage](#usage) - - [Kibana Actions Configuration](#kibana-actions-configuration) - - [Configuration Options](#configuration-options) - - [Adding Built-in Action Types to allowedHosts](#adding-built-in-action-types-to-allowedhosts) - - [Configuration Utilities](#configuration-utilities) - - [Action types](#action-types) - - [Methods](#methods) - - [Executor](#executor) - - [Example](#example) - - [RESTful API](#restful-api) - - [Firing actions](#firing-actions) - - [Accessing a scoped ActionsClient](#accessing-a-scoped-actionsclient) - - [actionsClient.enqueueExecution(options)](#actionsclientenqueueexecutionoptions) - - [Example](#example-1) - - [actionsClient.execute(options)](#actionsclientexecuteoptions) - - [Example](#example-2) - [Built-in Action Types](#built-in-action-types) - [ServiceNow](#servicenow) - [`params`](#params) @@ -56,8 +38,7 @@ Table of Contents - [`subActionParams (severity)`](#subactionparams-severity) - [Swimlane](#swimlane) - [`params`](#params-3) - - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-3) - - [`subActionParams (createRecord)`](#subactionparams-createRecord) + - [| severity | The severity of the incident. | string _(optional)_ |](#-severity-----the-severity-of-the-incident------string-optional-) - [Command Line Utility](#command-line-utility) - [Developing New Action Types](#developing-new-action-types) - [licensing](#licensing) @@ -106,8 +87,8 @@ This module provides utilities for interacting with the configuration. | ensureUriAllowed | _uri_: The URI you wish to validate is allowed | Validates whether the URI is allowed. This checks the configuration and validates that the hostname of the URI is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all URI's are allowed (using an "\*") then it will never throw. | No return value, throws if URI isn't allowed | | ensureHostnameAllowed | _hostname_: The Hostname you wish to validate is allowed | Validates whether the Hostname is allowed. This checks the configuration and validates that the hostname is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all Hostnames are allowed (using an "\*") then it will never throw | No return value, throws if Hostname isn't allowed . | | ensureActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Throws an error if the actionType is not enabled | No return value, throws if actionType isn't enabled | -| isRejectUnauthorizedCertificatesEnabled | _none_ | Returns value of `rejectUnauthorized` from configuration. | Boolean | -| getProxySettings | _none_ | If `proxyUrl` is set in the configuration, returns the proxy settings `proxyUrl`, `proxyHeaders` and `proxyRejectUnauthorizedCertificates`. Otherwise returns _undefined_. | Undefined or ProxySettings | +| isRejectUnauthorizedCertificatesEnabled | _none_ | Returns value of `rejectUnauthorized` from configuration. | Boolean | +| getProxySettings | _none_ | If `proxyUrl` is set in the configuration, returns the proxy settings `proxyUrl`, `proxyHeaders` and `proxyRejectUnauthorizedCertificates`. Otherwise returns _undefined_. | Undefined or ProxySettings | ## Action types @@ -117,17 +98,17 @@ This module provides utilities for interacting with the configuration. The following table describes the properties of the `options` object. -| Property | Description | Type | -| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | -| id | Unique identifier for the action type. For convention, ids starting with `.` are reserved for built in action types. We recommend using a convention like `.mySpecialAction` for your action types. | string | -| name | A user-friendly name for the action type. These will be displayed in dropdowns when chosing action types. | string | -| maxAttempts | The maximum number of times this action will attempt to execute when scheduled. | number | -| minimumLicenseRequired | The license required to use the action type. | string | +| Property | Description | Type | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | +| id | Unique identifier for the action type. For convention, ids starting with `.` are reserved for built in action types. We recommend using a convention like `.mySpecialAction` for your action types. | string | +| name | A user-friendly name for the action type. These will be displayed in dropdowns when chosing action types. | string | +| maxAttempts | The maximum number of times this action will attempt to execute when scheduled. | number | +| minimumLicenseRequired | The license required to use the action type. | string | | validate.params | When developing an action type, it needs to accept parameters to know what to do with the action. (Example `to`, `from`, `subject`, `body` of an email). See the current built-in email action type for an example of the state-of-the-art validation.

Technically, the value of this property should have a property named `validate()` which is a function that takes a params object to validate and returns a sanitized version of that object to pass to the execution function. Validation errors should be thrown from the `validate()` function and will be available as an error message | schema / validation function | -| validate.config | Similar to params, a config may be required when creating an action (for example `host` and `port` for an email server). | schema / validation function | -| validate.secrets | Similar to params, a secrets object may be required when creating an action (for example `user` and `password` for an email server). | schema / validation function | -| executor | This is where the code of an action type lives. This is a function gets called for executing an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below. | Function | -| renderParameterTemplates | Optionally define a function to provide custom rendering for this action type. | Function | +| validate.config | Similar to params, a config may be required when creating an action (for example `host` and `port` for an email server). | schema / validation function | +| validate.secrets | Similar to params, a secrets object may be required when creating an action (for example `user` and `password` for an email server). | schema / validation function | +| executor | This is where the code of an action type lives. This is a function gets called for executing an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below. | Function | +| renderParameterTemplates | Optionally define a function to provide custom rendering for this action type. | Function | **Important** - The config object is persisted in ElasticSearch and updated via the ElasticSearch update document API. This API allows "partial updates" - and this can cause issues with the encryption used on specified properties. So, a `validate()` function should return values for all configuration properties, so that partial updates do not occur. Setting property values to `null` rather than `undefined`, or not including a property in the config object, is all you need to do to ensure partial updates won't occur. @@ -137,15 +118,15 @@ This is the primary function for an action type. Whenever the action needs to ex **executor(options)** -| Property | Description | -| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| actionId | The action saved object id that the action type is executing for. | -| config | The action configuration. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type. | -| secrets | The decrypted secrets object given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the secrets object before being passed to the executor, define `validate.secrets` within the action type. | -| params | Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function. | -| services.scopedClusterClient | Use this to do Elasticsearch queries on the cluster Kibana connects to. Serves the same purpose as the normal IClusterClient, but exposes an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead.| -| services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.

The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). | -| services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log) +| Property | Description | +| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| actionId | The action saved object id that the action type is executing for. | +| config | The action configuration. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type. | +| secrets | The decrypted secrets object given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the secrets object before being passed to the executor, define `validate.secrets` within the action type. | +| params | Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function. | +| services.scopedClusterClient | Use this to do Elasticsearch queries on the cluster Kibana connects to. Serves the same purpose as the normal IClusterClient, but exposes an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. | +| services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.

The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). | +| services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log) | ### Example @@ -266,16 +247,16 @@ The [ServiceNow user documentation `params`](https://www.elastic.co/guide/en/kib The following table describes the properties of the `incident` object. -| Property | Description | Type | -| ----------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------- | -| short_description | The title of the incident. | string | -| description | The description of the incident. | string _(optional)_ | +| Property | Description | Type | +| ----------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------- | +| short_description | The title of the incident. | string | +| description | The description of the incident. | string _(optional)_ | | externalId | The ID of the incident in ServiceNow. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ | -| severity | The severity in ServiceNow. | string _(optional)_ | -| urgency | The urgency in ServiceNow. | string _(optional)_ | -| impact | The impact in ServiceNow. | string _(optional)_ | -| category | The category in ServiceNow. | string _(optional)_ | -| subcategory | The subcategory in ServiceNow. | string _(optional)_ | +| severity | The severity in ServiceNow. | string _(optional)_ | +| urgency | The urgency in ServiceNow. | string _(optional)_ | +| impact | The impact in ServiceNow. | string _(optional)_ | +| category | The category in ServiceNow. | string _(optional)_ | +| subcategory | The subcategory in ServiceNow. | string _(optional)_ | #### `subActionParams (getFields)` @@ -315,20 +296,20 @@ The [Jira user documentation `params`](https://www.elastic.co/guide/en/kibana/ma The following table describes the properties of the `incident` object. -| Property | Description | Type | -| ----------- | ---------------------------------------------------------------------------------------------------------------- | --------------------- | -| summary | The title of the issue. | string | -| description | The description of the issue. | string _(optional)_ | +| Property | Description | Type | +| ----------- | ------------------------------------------------------------------------------------------------------- | --------------------- | +| summary | The title of the issue. | string | +| description | The description of the issue. | string _(optional)_ | | externalId | The ID of the issue in Jira. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ | -| issueType | The ID of the issue type in Jira. | string _(optional)_ | -| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | -| labels | An array of labels. Labels cannot contain spaces. | string[] _(optional)_ | -| parent | The ID or key of the parent issue. Only for `Sub-task` issue types. | string _(optional)_ | +| issueType | The ID of the issue type in Jira. | string _(optional)_ | +| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | +| labels | An array of labels. Labels cannot contain spaces. | string[] _(optional)_ | +| parent | The ID or key of the parent issue. Only for `Sub-task` issue types. | string _(optional)_ | #### `subActionParams (getIncident)` -| Property | Description | Type | -| ---------- | --------------------------- | ------ | +| Property | Description | Type | +| ---------- | ---------------------------- | ------ | | externalId | The ID of the issue in Jira. | string | #### `subActionParams (issueTypes)` @@ -337,20 +318,20 @@ No parameters for the `issueTypes` subaction. Provide an empty object `{}`. #### `subActionParams (fieldsByIssueType)` -| Property | Description | Type | -| -------- | -------------------------------- | ------ | +| Property | Description | Type | +| -------- | --------------------------------- | ------ | | id | The ID of the issue type in Jira. | string | #### `subActionParams (issues)` -| Property | Description | Type | -| -------- | ----------------------- | ------ | +| Property | Description | Type | +| -------- | ------------------------ | ------ | | title | The title to search for. | string | #### `subActionParams (issue)` -| Property | Description | Type | -| -------- | --------------------------- | ------ | +| Property | Description | Type | +| -------- | ---------------------------- | ------ | | id | The ID of the issue in Jira. | string | #### `subActionParams (getFields)` @@ -364,10 +345,10 @@ The [IBM Resilient user documentation `params`](https://www.elastic.co/guide/en/ ### `params` -| Property | Description | Type | -| --------------- | -------------------------------------------------------------------------------------------------- | ------ | +| Property | Description | Type | +| --------------- | ------------------------------------------------------------------------------------------------- | ------ | | subAction | The subaction to perform. It can be `pushToService`, `getFields`, `incidentTypes`, and `severity. | string | -| subActionParams | The parameters of the subaction. | object | +| subActionParams | The parameters of the subaction. | object | #### `subActionParams (pushToService)` @@ -378,13 +359,13 @@ The [IBM Resilient user documentation `params`](https://www.elastic.co/guide/en/ The following table describes the properties of the `incident` object. -| Property | Description | Type | -| ------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- | -| name | The title of the incident. | string _(optional)_ | -| description | The description of the incident. | string _(optional)_ | +| Property | Description | Type | +| ------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------- | +| name | The title of the incident. | string _(optional)_ | +| description | The description of the incident. | string _(optional)_ | | externalId | The ID of the incident in IBM Resilient. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ | -| incidentTypes | An array with the IDs of IBM Resilient incident types. | number[] _(optional)_ | -| severityCode | IBM Resilient ID of the severity code. | number _(optional)_ | +| incidentTypes | An array with the IDs of IBM Resilient incident types. | number[] _(optional)_ | +| severityCode | IBM Resilient ID of the severity code. | number _(optional)_ | #### `subActionParams (getFields)` @@ -404,39 +385,30 @@ No parameters for the `severity` subaction. Provide an empty object `{}`. ### `params` -| Property | Description | Type | -| --------------- | -------------------------------------------------------------------------------------------------- | ------ | -| subAction | The subaction to perform. It can be `pushToService` or `createRecord`. | string | -| subActionParams | The parameters of the subaction. | object | - -#### `subActionParams (createRecord)` +| Property | Description | Type | +| --------------- | ---------------------------------------------------- | ------ | +| subAction | The subaction to perform. It can be `pushToService`. | string | +| subActionParams | The parameters of the subaction. | object | -| Property | Description | Type | -| ------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- | -| alertName | The alert name of the incident. | string | -| alertSource | The alert source of the incident. | string _(optional)_ | -| caseId | The case id of the incident. | string _(optional)_ | -| caseName | The case name of the incident. | string _(optional)_ | -| comments | The comments of the incident. | string _(optional)_ | -| severity | The severity of the incident. | string _(optional)_ | `subActionParams (pushToService)` | Property | Description | Type | | -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- | -| incident | The Swimlane incident. | object | +| incident | The Swimlane incident. | object | | comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ | + The following table describes the properties of the `incident` object. -| Property | Description | Type | -| ------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- | -| alertName | The alert name of the incident. | string | -| alertSource | The alert source of the incident. | string _(optional)_ | -| caseId | The case id of the incident. | string _(optional)_ | -| caseName | The case name of the incident. | string _(optional)_ | -| comments | The comments of the incident. | string _(optional)_ | -| severity | The severity of the incident. +| Property | Description | Type | +| ----------- | --------------------------------- | ------------------- | +| alertName | The alert name of the incident. | string _(optional)_ | +| alertSource | The alert source of the incident. | string _(optional)_ | +| caseId | The case id of the incident. | string _(optional)_ | +| caseName | The case name of the incident. | string _(optional)_ | +| description | The description of the incident. | string _(optional)_ | +| severity | The severity of the incident. | string _(optional)_ | --- # Command Line Utility From f7b8871457d054ec5c2e4dc3508e2bfcb47b5e3c Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 2 Jun 2021 11:02:03 +0300 Subject: [PATCH 55/96] Switch to button group --- .../swimlane/steps/swimlane_fields.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index 9560a0fce2a85..d147e742c371d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -11,7 +11,7 @@ import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption, - EuiRadioGroup, + EuiButtonGroup, } from '@elastic/eui'; import * as i18n from '../translations'; import { @@ -42,7 +42,7 @@ interface Props { errors: IErrorObject; } -const radios = [ +const connectorTypeButtons = [ { id: 'all', label: 'All' }, { id: 'alerts', label: 'Alerts' }, { id: 'cases', label: 'Cases' }, @@ -140,11 +140,13 @@ const SwimlaneFieldsComponent: React.FC = ({ return ( <> - editActionConfig('connectorType', type)} - name="connectorType" + isFullWidth /> {isValidFieldForConnector( From bd062c205aad9ff5da7036895739159a8305e8bc Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 2 Jun 2021 11:24:32 +0300 Subject: [PATCH 56/96] Better translation for required fields --- .../builtin_action_types/swimlane/helpers.ts | 11 +++++- .../swimlane/translations.ts | 35 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts index 56650dd8bd8cb..0825d0664f2d4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts @@ -18,6 +18,15 @@ const alertsFields = [ ...alertsRequiredFields, ]; +const translationMapping: Record = { + caseNameConfig: i18n.SW_REQUIRED_CASE_NAME, + descriptionConfig: i18n.SW_REQUIRED_DESCRIPTION, + commentsConfig: i18n.SW_REQUIRED_COMMENTS, + alertNameConfig: i18n.SW_REQUIRED_ALERT_NAME, + alertSourceConfig: i18n.SW_REQUIRED_ALERT_SOURCE, + severityConfig: i18n.SW_REQUIRED_SEVERITY, +}; + export const isValidFieldForConnector = ( connector: SwimlaneConnectorType, field: string @@ -43,7 +52,7 @@ export const validateMappingForConnector = ( : casesRequiredFields.includes(key); if (isFieldRequired && mapping != null && mapping[key] == null) { - errors = { ...errors, [key]: i18n.SW_FIELD_MAPPING_IS_REQUIRED }; + errors = { ...errors, [key]: translationMapping[key] }; } } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts index 21b3301c1067a..9933c4dd84f2b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts @@ -215,3 +215,38 @@ export const EMPTY_MAPPING_WARNING_DESC = i18n.translate( defaultMessage: `The mapping of the connector is empty. Create a connector of type Alerts before pushing to a case.`, } ); + +export const SW_REQUIRED_ALERT_SOURCE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertSource', + { + defaultMessage: 'Alert Source is required.', + } +); + +export const SW_REQUIRED_SEVERITY = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredSeverity', + { + defaultMessage: 'Severity is required.', + } +); + +export const SW_REQUIRED_CASE_NAME = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredCaseName', + { + defaultMessage: 'Case Name is required.', + } +); + +export const SW_REQUIRED_COMMENTS = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredComments', + { + defaultMessage: 'Comments are required.', + } +); + +export const SW_REQUIRED_DESCRIPTION = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredDescription', + { + defaultMessage: 'Description is required.', + } +); From 835f62bb5171bd0b13ed1b4cbfd4aaba424c7033 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 2 Jun 2021 16:18:45 +0300 Subject: [PATCH 57/96] Change buttons width --- .../builtin_action_types/swimlane/steps/swimlane_fields.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index d147e742c371d..8006a049d8c70 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -146,7 +146,6 @@ const SwimlaneFieldsComponent: React.FC = ({ options={connectorTypeButtons} idSelected={connectorType} onChange={(type) => editActionConfig('connectorType', type)} - isFullWidth /> {isValidFieldForConnector( From 68fc3ef54785c06ab1cb393854a002e3cba83822 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 2 Jun 2021 16:22:54 +0300 Subject: [PATCH 58/96] Compress buttons --- .../builtin_action_types/swimlane/steps/swimlane_fields.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index 8006a049d8c70..9ece609a86b4f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -146,6 +146,7 @@ const SwimlaneFieldsComponent: React.FC = ({ options={connectorTypeButtons} idSelected={connectorType} onChange={(type) => editActionConfig('connectorType', type)} + buttonSize="compressed" /> {isValidFieldForConnector( From cc7d41f5c7028685d43c5d9b47fd1bf8d7e62134 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 3 Jun 2021 10:42:02 +0300 Subject: [PATCH 59/96] Make case id required --- .../components/builtin_action_types/swimlane/helpers.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts index 0825d0664f2d4..a8f6090cc5552 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts @@ -8,8 +8,13 @@ import { SwimlaneConnectorType, SwimlaneMappingConfig } from './types'; import * as i18n from './translations'; -const casesRequiredFields = ['caseNameConfig', 'descriptionConfig', 'commentsConfig']; -const casesFields = ['caseIdConfig', ...casesRequiredFields]; +const casesRequiredFields = [ + 'caseNameConfig', + 'descriptionConfig', + 'commentsConfig', + 'caseIdConfig', +]; +const casesFields = [...casesRequiredFields]; const alertsRequiredFields = ['alertNameConfig']; const alertsFields = [ 'alertSourceConfig', From bc9ac93ed361433f4173b71bff01ce96fcddb7c7 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 3 Jun 2021 10:48:02 +0300 Subject: [PATCH 60/96] Rename to rule name --- .../swimlane/helpers.test.ts | 4 ++-- .../builtin_action_types/swimlane/mocks.ts | 6 +++--- .../builtin_action_types/swimlane/schema.ts | 2 +- .../swimlane/service.test.ts | 4 ++-- .../builtin_action_types/swimlane/helpers.ts | 4 ++-- .../builtin_action_types/swimlane/mocks.ts | 6 +++--- .../swimlane/steps/swimlane_fields.tsx | 16 ++++++++-------- .../swimlane/swimlane.test.tsx | 2 +- .../swimlane/swimlane_params.tsx | 4 ++-- .../swimlane/translations.ts | 8 ++++---- .../builtin_action_types/swimlane/types.ts | 2 +- .../actions/builtin_action_types/swimlane.ts | 6 +++--- .../actions/builtin_action_types/swimlane.ts | 8 ++++---- 13 files changed, 36 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts index a456a52a8d03e..9ecaa7d0a4c53 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts @@ -13,7 +13,7 @@ describe('Create Record Mapping', () => { test('Mapping is Successful', () => { const params = { - alertName: 'Alert Name', + alertName: 'Rule Name', severity: 'Critical', alertSource: 'Elastic', caseName: 'Case Name', @@ -24,7 +24,7 @@ describe('Create Record Mapping', () => { }; const data = getBodyForEventAction(appId, mappings, params); expect(data?.values?.[mappings.alertSourceConfig?.id ?? 0]).toEqual(params.alertSource); - expect(data?.values?.[mappings.alertNameConfig.id]).toEqual(params.alertName); + expect(data?.values?.[mappings.ruleNameConfig.id]).toEqual(params.alertName); expect(data?.values?.[mappings.caseNameConfig?.id ?? 0]).toEqual(params.caseName); expect(data?.values?.[mappings.caseIdConfig?.id ?? 0]).toEqual(params.caseId); expect(data?.values?.[mappings.commentsConfig?.id ?? 0]).toEqual(params.comments); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts index f1185284f3562..e6fe4d195e618 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts @@ -22,8 +22,8 @@ export const applicationFields = [ }, { id: 'adnfls', - name: 'Alert Name', - key: 'alert-name', + name: 'Rule Name', + key: 'rule-name', fieldType: 'text', }, { @@ -55,7 +55,7 @@ export const applicationFields = [ export const mappings = { alertSourceConfig: applicationFields[0], severityConfig: applicationFields[1], - alertNameConfig: applicationFields[2], + ruleNameConfig: applicationFields[2], caseIdConfig: applicationFields[3], caseNameConfig: applicationFields[4], commentsConfig: applicationFields[5], diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts index d499739554311..1f3c39fef1e8b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -17,7 +17,7 @@ export const ConfigMap = { export const ConfigMapSchema = schema.object(ConfigMap); export const ConfigMapping = { - alertNameConfig: schema.nullable(ConfigMapSchema), + ruleNameConfig: schema.nullable(ConfigMapSchema), alertSourceConfig: schema.nullable(ConfigMapSchema), caseIdConfig: schema.nullable(ConfigMapSchema), caseNameConfig: schema.nullable(ConfigMapSchema), diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts index f767277fb2b2a..1fc7b57d2bc12 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts @@ -45,7 +45,7 @@ describe('Swimlane Service', () => { }; const incident = { - alertName: 'Alert Name', + alertName: 'Rule Name', alertSource: 'Alert Source', caseId: 'Case Id', caseName: 'Case Name', @@ -182,7 +182,7 @@ describe('Swimlane Service', () => { data: { applicationId: config.appId, values: { - [mappings.alertNameConfig.id]: 'Alert Name', + [mappings.ruleNameConfig.id]: 'Rule Name', [mappings.alertSourceConfig.id]: 'Alert Source', [mappings.caseNameConfig.id]: 'Case Name', [mappings.caseIdConfig.id]: 'Case Id', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts index a8f6090cc5552..96cb498f20852 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts @@ -15,7 +15,7 @@ const casesRequiredFields = [ 'caseIdConfig', ]; const casesFields = [...casesRequiredFields]; -const alertsRequiredFields = ['alertNameConfig']; +const alertsRequiredFields = ['ruleNameConfig']; const alertsFields = [ 'alertSourceConfig', 'severityConfig', @@ -27,7 +27,7 @@ const translationMapping: Record = { caseNameConfig: i18n.SW_REQUIRED_CASE_NAME, descriptionConfig: i18n.SW_REQUIRED_DESCRIPTION, commentsConfig: i18n.SW_REQUIRED_COMMENTS, - alertNameConfig: i18n.SW_REQUIRED_ALERT_NAME, + ruleNameConfig: i18n.SW_REQUIRED_RULE_NAME, alertSourceConfig: i18n.SW_REQUIRED_ALERT_SOURCE, severityConfig: i18n.SW_REQUIRED_SEVERITY, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts index efc437221ecae..ac1f4dbd21e4b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts @@ -20,8 +20,8 @@ export const applicationFields = [ }, { id: 'adnfls', - name: 'Alert Name', - key: 'alert-name', + name: 'Rule Name', + key: 'rule-name', fieldType: 'text', }, { @@ -53,7 +53,7 @@ export const applicationFields = [ export const mappings = { alertSourceConfig: applicationFields[0], severityConfig: applicationFields[1], - alertNameConfig: applicationFields[2], + ruleNameConfig: applicationFields[2], caseIdConfig: applicationFields[3], caseNameConfig: applicationFields[4], commentsConfig: applicationFields[5], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index 9ece609a86b4f..c7c4d90e574f6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -85,7 +85,7 @@ const SwimlaneFieldsComponent: React.FC = ({ () => ({ alertSourceConfig: createSelectedOption(mappings?.alertSourceConfig), severityConfig: createSelectedOption(mappings?.severityConfig), - alertNameConfig: createSelectedOption(mappings?.alertNameConfig), + ruleNameConfig: createSelectedOption(mappings?.ruleNameConfig), caseIdConfig: createSelectedOption(mappings?.caseIdConfig), caseNameConfig: createSelectedOption(mappings?.caseNameConfig), commentsConfig: createSelectedOption(mappings?.commentsConfig), @@ -194,23 +194,23 @@ const SwimlaneFieldsComponent: React.FC = ({ )} - {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'alertNameConfig') && ( + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'ruleNameConfig') && ( <> editMappings('alertNameConfig', e)} - isInvalid={mappingErrors?.alertNameConfig != null} + onChange={(e) => editMappings('ruleNameConfig', e)} + isInvalid={mappingErrors?.ruleNameConfig != null} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx index 74ffb2aab2629..ddac5618f188a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx @@ -66,7 +66,7 @@ describe('swimlane action params validation', () => { test('action params validation succeeds when action params is valid', () => { const actionParams = { subActionParams: { - alertName: 'Alert Name', + alertName: 'Rule Name', }, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx index 1355b2f98cf30..2afde99fa45dc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx @@ -39,13 +39,13 @@ const SwimlaneParamsFields: React.FunctionComponent ({ - hasAlertName: mappings.alertNameConfig != null, + hasAlertName: mappings.ruleNameConfig != null, hasAlertSource: mappings.alertSourceConfig != null, hasComments: mappings.commentsConfig != null, hasSeverity: mappings.severityConfig != null, }), [ - mappings.alertNameConfig, + mappings.ruleNameConfig, mappings.alertSourceConfig, mappings.commentsConfig, mappings.severityConfig, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts index 9933c4dd84f2b..64d8695bbf156 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts @@ -21,10 +21,10 @@ export const SW_ACTION_TYPE_TITLE = i18n.translate( } ); -export const SW_REQUIRED_ALERT_NAME = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertName', +export const SW_REQUIRED_RULE_NAME = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredRuleName', { - defaultMessage: 'Alert Name is required.', + defaultMessage: 'Rule Name is required.', } ); @@ -131,7 +131,7 @@ export const SW_MAPPING_DESCRIPTION_TEXT_FIELD_LABEL = i18n.translate( export const SW_ALERT_NAME_FIELD_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertNameFieldLabel', { - defaultMessage: 'Alert Name', + defaultMessage: 'Rule Name', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts index e81acd84ae223..bb7c95caf870e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts @@ -26,7 +26,7 @@ export interface SwimlaneMappingConfig { severityConfig: SwimlaneFieldMappingConfig; caseNameConfig: SwimlaneFieldMappingConfig; caseIdConfig: SwimlaneFieldMappingConfig; - alertNameConfig: SwimlaneFieldMappingConfig; + ruleNameConfig: SwimlaneFieldMappingConfig; commentsConfig: SwimlaneFieldMappingConfig; descriptionConfig: SwimlaneFieldMappingConfig; [key: string]: SwimlaneFieldMappingConfig; diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts index bf98861c1e05e..6e25c8bd2d288 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts @@ -35,10 +35,10 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { key: 'severity', fieldType: 'text', }, - alertNameConfig: { + ruleNameConfig: { id: 'adnfls', - name: 'Alert Name', - key: 'alert-name', + name: 'Rule Name', + key: 'rule-name', fieldType: 'text', }, caseIdConfig: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts index 7767ae89f56f4..751b9b064959e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts @@ -42,10 +42,10 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { key: 'severity', fieldType: 'text', }, - alertNameConfig: { + ruleNameConfig: { id: 'adnfls', - name: 'Alert Name', - key: 'alert-name', + name: 'Rule Name', + key: 'rule-name', fieldType: 'text', }, caseIdConfig: { @@ -81,7 +81,7 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { subAction: 'pushToService', subActionParams: { incident: { - alertName: 'Alert Name', + alertName: 'Rule Name', severity: 'Critical', alertSource: 'Elastic', caseName: 'Case Name', From 26d3523a5f3acff5165cc960ebdb7a0b83bac630 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 3 Jun 2021 10:51:22 +0300 Subject: [PATCH 61/96] Fix translation --- .../components/builtin_action_types/swimlane/helpers.ts | 1 + .../builtin_action_types/swimlane/translations.ts | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts index 96cb498f20852..ba1a0374781fb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts @@ -24,6 +24,7 @@ const alertsFields = [ ]; const translationMapping: Record = { + caseIdConfig: i18n.SW_REQUIRED_CASE_ID, caseNameConfig: i18n.SW_REQUIRED_CASE_NAME, descriptionConfig: i18n.SW_REQUIRED_DESCRIPTION, commentsConfig: i18n.SW_REQUIRED_COMMENTS, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts index 64d8695bbf156..57d7d240cbe62 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts @@ -237,6 +237,13 @@ export const SW_REQUIRED_CASE_NAME = i18n.translate( } ); +export const SW_REQUIRED_CASE_ID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredCaseID', + { + defaultMessage: 'Case ID is required.', + } +); + export const SW_REQUIRED_COMMENTS = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredComments', { From ed87bc4b2f87b50fa0fd99bbbc25a37b4aab17d6 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 3 Jun 2021 10:57:42 +0300 Subject: [PATCH 62/96] Add alert id field --- .../builtin_action_types/swimlane/schema.ts | 1 + .../builtin_action_types/swimlane/helpers.ts | 3 ++- .../swimlane/steps/swimlane_fields.tsx | 24 ++++++++++++++++++- .../swimlane/translations.ts | 18 ++++++++++++-- .../builtin_action_types/swimlane/types.ts | 1 + 5 files changed, 43 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts index 1f3c39fef1e8b..d4fc2477b1c02 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -18,6 +18,7 @@ export const ConfigMapSchema = schema.object(ConfigMap); export const ConfigMapping = { ruleNameConfig: schema.nullable(ConfigMapSchema), + alertIdConfig: schema.nullable(ConfigMapSchema), alertSourceConfig: schema.nullable(ConfigMapSchema), caseIdConfig: schema.nullable(ConfigMapSchema), caseNameConfig: schema.nullable(ConfigMapSchema), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts index ba1a0374781fb..4e9d20ad0012d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts @@ -15,7 +15,7 @@ const casesRequiredFields = [ 'caseIdConfig', ]; const casesFields = [...casesRequiredFields]; -const alertsRequiredFields = ['ruleNameConfig']; +const alertsRequiredFields = ['ruleNameConfig', 'alertIdConfig']; const alertsFields = [ 'alertSourceConfig', 'severityConfig', @@ -25,6 +25,7 @@ const alertsFields = [ const translationMapping: Record = { caseIdConfig: i18n.SW_REQUIRED_CASE_ID, + alertIdConfig: i18n.SW_REQUIRED_ALERT_ID, caseNameConfig: i18n.SW_REQUIRED_CASE_NAME, descriptionConfig: i18n.SW_REQUIRED_DESCRIPTION, commentsConfig: i18n.SW_REQUIRED_COMMENTS, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index c7c4d90e574f6..439d40556dc60 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -84,6 +84,7 @@ const SwimlaneFieldsComponent: React.FC = ({ const state = useMemo( () => ({ alertSourceConfig: createSelectedOption(mappings?.alertSourceConfig), + alertIdConfig: createSelectedOption(mappings?.alertIdConfig), severityConfig: createSelectedOption(mappings?.severityConfig), ruleNameConfig: createSelectedOption(mappings?.ruleNameConfig), caseIdConfig: createSelectedOption(mappings?.caseIdConfig), @@ -199,7 +200,7 @@ const SwimlaneFieldsComponent: React.FC = ({ @@ -215,6 +216,27 @@ const SwimlaneFieldsComponent: React.FC = ({ )} + {isValidFieldForConnector(connectorType as SwimlaneConnectorType.All, 'alertIdConfig') && ( + <> + + editMappings('alertIdConfig', e)} + isInvalid={mappingErrors?.alertIdConfig != null} + /> + + + )} {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'caseIdConfig') && ( <> Date: Thu, 3 Jun 2021 11:03:27 +0300 Subject: [PATCH 63/96] Reorder fields --- .../swimlane/steps/swimlane_fields.tsx | 78 +++++++++---------- .../swimlane/translations.ts | 2 +- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index 439d40556dc60..8f0575936bba5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -150,89 +150,89 @@ const SwimlaneFieldsComponent: React.FC = ({ buttonSize="compressed" /> - {isValidFieldForConnector( - connectorType as SwimlaneConnectorType.All, - 'alertSourceConfig' - ) && ( + {isValidFieldForConnector(connectorType as SwimlaneConnectorType.All, 'alertIdConfig') && ( <> editMappings('alertSourceConfig', e)} - isInvalid={mappingErrors?.alertSourceConfig != null} + data-test-subj="swimlaneAlertIdInput" + onChange={(e) => editMappings('alertIdConfig', e)} + isInvalid={mappingErrors?.alertIdConfig != null} /> )} - {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'severityConfig') && ( + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'ruleNameConfig') && ( <> editMappings('severityConfig', e)} - isInvalid={mappingErrors?.severityConfig != null} + data-test-subj="swimlaneAlertNameInput" + onChange={(e) => editMappings('ruleNameConfig', e)} + isInvalid={mappingErrors?.ruleNameConfig != null} /> )} - {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'ruleNameConfig') && ( + {isValidFieldForConnector( + connectorType as SwimlaneConnectorType.All, + 'alertSourceConfig' + ) && ( <> editMappings('ruleNameConfig', e)} - isInvalid={mappingErrors?.ruleNameConfig != null} + data-test-subj="swimlaneAlertSourceInput" + onChange={(e) => editMappings('alertSourceConfig', e)} + isInvalid={mappingErrors?.alertSourceConfig != null} /> )} - {isValidFieldForConnector(connectorType as SwimlaneConnectorType.All, 'alertIdConfig') && ( + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'severityConfig') && ( <> editMappings('alertIdConfig', e)} - isInvalid={mappingErrors?.alertIdConfig != null} + data-test-subj="swimlaneSeverityInput" + onChange={(e) => editMappings('severityConfig', e)} + isInvalid={mappingErrors?.severityConfig != null} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts index 1b1e5975efaeb..5a918a0cdd473 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts @@ -110,7 +110,7 @@ export const SW_MAPPING_TITLE_TEXT_FIELD_LABEL = i18n.translate( export const SW_ALERT_SOURCE_FIELD_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertSourceFieldLabel', { - defaultMessage: 'Source', + defaultMessage: 'Alert Source', } ); From d0d4f77b1b750ad4ed0864377ed30746aa299abf Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 3 Jun 2021 11:38:43 +0300 Subject: [PATCH 64/96] Fixes --- .../builtin_action_types/swimlane/api.test.ts | 3 ++- .../swimlane/helpers.test.ts | 5 ++-- .../builtin_action_types/swimlane/mocks.ts | 10 +++++++- .../builtin_action_types/swimlane/schema.ts | 3 ++- .../swimlane/service.test.ts | 4 +++- .../swimlane/swimlane.test.tsx | 4 ++-- .../swimlane/swimlane.tsx | 6 ++--- .../swimlane/swimlane_params.test.tsx | 8 ++++--- .../swimlane/swimlane_params.tsx | 24 +++++++++---------- .../actions/builtin_action_types/swimlane.ts | 2 +- 10 files changed, 42 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts index 679a062fcb9b6..03ddfff013faf 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts @@ -12,7 +12,8 @@ import { Logger } from '@kbn/logging'; let mockedLogger: jest.Mocked; const params = { - alertName: 'alert name', + ruleName: 'rule name', + alertId: '123456', caseName: 'case name', severity: 'critical', alertSource: 'elastic', diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts index 9ecaa7d0a4c53..4657c42b1e37b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts @@ -13,7 +13,8 @@ describe('Create Record Mapping', () => { test('Mapping is Successful', () => { const params = { - alertName: 'Rule Name', + alertId: 'al123', + ruleName: 'Rule Name', severity: 'Critical', alertSource: 'Elastic', caseName: 'Case Name', @@ -24,7 +25,7 @@ describe('Create Record Mapping', () => { }; const data = getBodyForEventAction(appId, mappings, params); expect(data?.values?.[mappings.alertSourceConfig?.id ?? 0]).toEqual(params.alertSource); - expect(data?.values?.[mappings.ruleNameConfig.id]).toEqual(params.alertName); + expect(data?.values?.[mappings.ruleNameConfig.id]).toEqual(params.ruleName); expect(data?.values?.[mappings.caseNameConfig?.id ?? 0]).toEqual(params.caseName); expect(data?.values?.[mappings.caseIdConfig?.id ?? 0]).toEqual(params.caseId); expect(data?.values?.[mappings.commentsConfig?.id ?? 0]).toEqual(params.comments); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts index e6fe4d195e618..654b39225ab8d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts @@ -50,6 +50,12 @@ export const applicationFields = [ key: 'description', fieldType: 'text', }, + { + id: 'dfnkls', + name: 'Alert ID', + key: 'alert-id', + fieldType: 'text', + }, ]; export const mappings = { @@ -60,6 +66,7 @@ export const mappings = { caseNameConfig: applicationFields[4], commentsConfig: applicationFields[5], descriptionConfig: applicationFields[6], + alertIdConfig: applicationFields[7], }; export const getApplicationResponse = { fields: applicationFields }; @@ -93,12 +100,13 @@ const externalServiceMock = { }; const executorParams = { - alertName: 'alert-name', + ruleName: 'rule-name', alertSource: 'alert-source', caseId: 'case-id', caseName: 'case-name', comments: 'comments', severity: 'severity', + alertId: 'alert-id', }; const apiParams = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts index d4fc2477b1c02..69f8e4e16bdcf 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -45,7 +45,8 @@ export const SwimlaneSecretsConfiguration = { export const SwimlaneSecretsConfigurationSchema = schema.object(SwimlaneSecretsConfiguration); const SwimlaneFields = { - alertName: schema.nullable(schema.string()), + alertId: schema.nullable(schema.string()), + ruleName: schema.nullable(schema.string()), alertSource: schema.nullable(schema.string()), caseId: schema.nullable(schema.string()), caseName: schema.nullable(schema.string()), diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts index 1fc7b57d2bc12..fd77144219d14 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts @@ -45,7 +45,7 @@ describe('Swimlane Service', () => { }; const incident = { - alertName: 'Rule Name', + ruleName: 'Rule Name', alertSource: 'Alert Source', caseId: 'Case Id', caseName: 'Case Name', @@ -53,6 +53,7 @@ describe('Swimlane Service', () => { severity: 'Severity', externalId: null, description: 'Description', + alertId: 'Alert Id', }; beforeAll(() => { @@ -189,6 +190,7 @@ describe('Swimlane Service', () => { [mappings.commentsConfig.id]: 'Comments', [mappings.severityConfig.id]: 'Severity', [mappings.descriptionConfig.id]: 'Description', + [mappings.alertIdConfig.id]: 'Alert Id', }, }, url: `${config.apiUrl.slice(0, -1)}/api/app/${config.appId}/record`, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx index ddac5618f188a..d205336719275 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx @@ -66,13 +66,13 @@ describe('swimlane action params validation', () => { test('action params validation succeeds when action params is valid', () => { const actionParams = { subActionParams: { - alertName: 'Rule Name', + ruleName: 'Rule Name', }, }; expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { - 'subActionParams.incident.alertName': [], + 'subActionParams.incident.ruleName': [], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx index 062ea13383dd9..21615ec812132 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx @@ -79,7 +79,7 @@ export function getActionType(): ActionTypeModel< }, validateParams: (actionParams: SwimlaneActionParams): GenericValidationResult => { const errors = { - 'subActionParams.incident.alertName': new Array(), + 'subActionParams.incident.ruleName': new Array(), }; const validationResult = { errors, @@ -87,9 +87,9 @@ export function getActionType(): ActionTypeModel< if ( actionParams.subActionParams && actionParams.subActionParams.incident && - !actionParams.subActionParams.incident.alertName?.length + !actionParams.subActionParams.incident.ruleName?.length ) { - errors['subActionParams.incident.alertName'].push(i18n.SW_REQUIRED_ALERT_NAME); + errors['subActionParams.incident.ruleName'].push(i18n.SW_REQUIRED_RULE_NAME); } return validationResult; }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx index c87320a2f356a..7c11027a071fd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx @@ -17,13 +17,14 @@ describe('SwimlaneParamsFields renders', () => { subAction: 'pushToService', subActionParams: { incident: { - alertName: 'alert name', + ruleName: 'rule name', alertSource: 'alert source', caseId: '3456789', caseName: 'my case name', severity: 'critical', description: 'case desc', externalId: null, + alertId: '3456789', }, comments: [], }, @@ -42,12 +43,13 @@ describe('SwimlaneParamsFields renders', () => { {}} @@ -57,6 +59,6 @@ describe('SwimlaneParamsFields renders', () => { expect(wrapper.find('[data-test-subj="severity"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="comments"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="alertSource"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="alertName"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="ruleName"]').length > 0).toBeTruthy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx index 2afde99fa45dc..d14bdad8accaf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx @@ -37,9 +37,9 @@ const SwimlaneParamsFields: React.FunctionComponent ({ - hasAlertName: mappings.ruleNameConfig != null, + hasRuleName: mappings.ruleNameConfig != null, hasAlertSource: mappings.alertSourceConfig != null, hasComments: mappings.commentsConfig != null, hasSeverity: mappings.severityConfig != null, @@ -54,7 +54,7 @@ const SwimlaneParamsFields: React.FunctionComponent { @@ -118,26 +118,26 @@ const SwimlaneParamsFields: React.FunctionComponent - {hasAlertName && ( + {hasRuleName && ( <> 0 && - incident.alertName !== undefined + errors['subActionParams.incident.ruleName']?.length > 0 && + incident.ruleName !== undefined } - label={i18n.SW_ALERT_NAME_FIELD_LABEL} + label={i18n.SW_RULE_NAME_FIELD_LABEL} > diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts index 751b9b064959e..aef8b9d75a5a7 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts @@ -81,7 +81,7 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { subAction: 'pushToService', subActionParams: { incident: { - alertName: 'Rule Name', + ruleName: 'Rule Name', severity: 'Critical', alertSource: 'Elastic', caseName: 'Case Name', From b283be811d66894dba211eeec894cab35f4373a6 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 3 Jun 2021 12:49:41 +0300 Subject: [PATCH 65/96] Fix validation --- .../builtin_action_types/swimlane/helpers.ts | 21 ++++----- .../swimlane/steps/swimlane_connection.tsx | 26 ++++++++--- .../swimlane/steps/swimlane_fields.tsx | 43 +++++++++++-------- 3 files changed, 58 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts index 4e9d20ad0012d..89de49f55fad8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts @@ -50,18 +50,19 @@ export const isValidFieldForConnector = ( export const validateMappingForConnector = ( connector: SwimlaneConnectorType, mapping: SwimlaneMappingConfig -): Record => - Object.keys(mapping ?? {}).reduce((errors, key) => { - if (connector !== SwimlaneConnectorType.All) { - const isFieldRequired = - connector === SwimlaneConnectorType.Alerts - ? alertsRequiredFields.includes(key) - : casesRequiredFields.includes(key); +): Record => { + if (connector === SwimlaneConnectorType.All) { + return {}; + } - if (isFieldRequired && mapping != null && mapping[key] == null) { - errors = { ...errors, [key]: translationMapping[key] }; - } + const requiredFields = + connector === SwimlaneConnectorType.Alerts ? alertsRequiredFields : casesRequiredFields; + + return requiredFields.reduce((errors, field) => { + if (mapping == null || (mapping != null && mapping[field] == null)) { + errors = { ...errors, [field]: translationMapping[field] }; } return errors; }, {} as Record); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx index bf45feed93312..f489e7b916a95 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx @@ -94,27 +94,43 @@ const SwimlaneConnectionComponent: React.FunctionComponent = ({ } }, [apiToken, editActionSecrets]); - const isInvalid = errors.apiToken.length > 0 && apiToken !== undefined; + const isApiUrlInvalid = errors.apiUrl.length > 0 && apiToken !== undefined; + const isAppIdInvalid = errors.appId.length > 0 && apiToken !== undefined; + const isApiTokenInvalid = errors.apiToken.length > 0 && apiToken !== undefined; return ( <> - + onChangeConfig(e, 'apiUrl')} onBlur={() => onBlurConfig('apiUrl')} /> - + onChangeConfig(e, 'appId')} onBlur={() => onBlurConfig('appId')} @@ -135,7 +151,7 @@ const SwimlaneConnectionComponent: React.FunctionComponent = ({ } error={errors.apiToken} - isInvalid={isInvalid} + isInvalid={isApiTokenInvalid} label={i18n.SW_API_TOKEN_TEXT_FIELD_LABEL} > <> @@ -161,7 +177,7 @@ const SwimlaneConnectionComponent: React.FunctionComponent = ({ )} = ({ errors, }) => { const { mappings, connectorType = SwimlaneConnectorType.All } = action.config; + const prevConnectorType = useRef(connectorType); + const hasChangedConnectorType = connectorType !== prevConnectorType.current; + const [fieldTypeMap, fieldIdMap] = useMemo( () => fields.reduce( @@ -138,6 +141,12 @@ const SwimlaneFieldsComponent: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => editActionConfig('connectorType', connectorType), []); + useEffect(() => { + if (connectorType !== prevConnectorType.current) { + prevConnectorType.current = connectorType; + } + }, [connectorType]); + return ( <> @@ -157,7 +166,7 @@ const SwimlaneFieldsComponent: React.FC = ({ fullWidth label={i18n.SW_ALERT_ID_FIELD_LABEL} error={mappingErrors?.alertIdConfig} - isInvalid={mappingErrors?.alertIdConfig != null} + isInvalid={mappingErrors?.alertIdConfig != null && !hasChangedConnectorType} > = ({ singleSelection={SINGLE_SELECTION} data-test-subj="swimlaneAlertIdInput" onChange={(e) => editMappings('alertIdConfig', e)} - isInvalid={mappingErrors?.alertIdConfig != null} + isInvalid={mappingErrors?.alertIdConfig != null && !hasChangedConnectorType} /> @@ -178,7 +187,7 @@ const SwimlaneFieldsComponent: React.FC = ({ fullWidth label={i18n.SW_RULE_NAME_FIELD_LABEL} error={mappingErrors?.ruleNameConfig} - isInvalid={mappingErrors?.ruleNameConfig != null} + isInvalid={mappingErrors?.ruleNameConfig != null && !hasChangedConnectorType} > = ({ singleSelection={SINGLE_SELECTION} data-test-subj="swimlaneAlertNameInput" onChange={(e) => editMappings('ruleNameConfig', e)} - isInvalid={mappingErrors?.ruleNameConfig != null} + isInvalid={mappingErrors?.ruleNameConfig != null && !hasChangedConnectorType} /> @@ -202,7 +211,7 @@ const SwimlaneFieldsComponent: React.FC = ({ fullWidth label={i18n.SW_ALERT_SOURCE_FIELD_LABEL} error={mappingErrors?.alertSourceConfig} - isInvalid={mappingErrors?.alertSourceConfig != null} + isInvalid={mappingErrors?.alertSourceConfig != null && !hasChangedConnectorType} > = ({ singleSelection={SINGLE_SELECTION} data-test-subj="swimlaneAlertSourceInput" onChange={(e) => editMappings('alertSourceConfig', e)} - isInvalid={mappingErrors?.alertSourceConfig != null} + isInvalid={mappingErrors?.alertSourceConfig != null && !hasChangedConnectorType} /> @@ -223,7 +232,7 @@ const SwimlaneFieldsComponent: React.FC = ({ fullWidth label={i18n.SW_SEVERITY_FIELD_LABEL} error={mappingErrors?.severityConfig} - isInvalid={mappingErrors?.severityConfig != null} + isInvalid={mappingErrors?.severityConfig != null && !hasChangedConnectorType} > = ({ singleSelection={SINGLE_SELECTION} data-test-subj="swimlaneSeverityInput" onChange={(e) => editMappings('severityConfig', e)} - isInvalid={mappingErrors?.severityConfig != null} + isInvalid={mappingErrors?.severityConfig != null && !hasChangedConnectorType} /> @@ -244,7 +253,7 @@ const SwimlaneFieldsComponent: React.FC = ({ fullWidth label={i18n.SW_CASE_ID_FIELD_LABEL} error={mappingErrors?.caseIdConfig} - isInvalid={mappingErrors?.caseIdConfig != null} + isInvalid={mappingErrors?.caseIdConfig != null && !hasChangedConnectorType} > = ({ singleSelection={SINGLE_SELECTION} data-test-subj="swimlaneCaseIdConfig" onChange={(e) => editMappings('caseIdConfig', e)} - isInvalid={mappingErrors?.caseIdConfig != null} + isInvalid={mappingErrors?.caseIdConfig != null && !hasChangedConnectorType} /> @@ -265,7 +274,7 @@ const SwimlaneFieldsComponent: React.FC = ({ fullWidth label={i18n.SW_CASE_NAME_FIELD_LABEL} error={mappingErrors?.caseNameConfig} - isInvalid={mappingErrors?.caseNameConfig != null} + isInvalid={mappingErrors?.caseNameConfig != null && !hasChangedConnectorType} > = ({ singleSelection={SINGLE_SELECTION} data-test-subj="swimlaneCaseNameConfig" onChange={(e) => editMappings('caseNameConfig', e)} - isInvalid={mappingErrors?.caseNameConfig != null} + isInvalid={mappingErrors?.caseNameConfig != null && !hasChangedConnectorType} /> @@ -286,7 +295,7 @@ const SwimlaneFieldsComponent: React.FC = ({ fullWidth label={i18n.SW_COMMENTS_FIELD_LABEL} error={mappingErrors?.commentsConfig} - isInvalid={mappingErrors?.commentsConfig != null} + isInvalid={mappingErrors?.commentsConfig != null && !hasChangedConnectorType} > = ({ singleSelection={SINGLE_SELECTION} data-test-subj="swimlaneCommentsConfig" onChange={(e) => editMappings('commentsConfig', e)} - isInvalid={mappingErrors?.commentsConfig != null} + isInvalid={mappingErrors?.commentsConfig != null && !hasChangedConnectorType} /> @@ -307,7 +316,7 @@ const SwimlaneFieldsComponent: React.FC = ({ fullWidth label={i18n.SW_DESCRIPTION_FIELD_LABEL} error={mappingErrors?.descriptionConfig} - isInvalid={mappingErrors?.descriptionConfig != null} + isInvalid={mappingErrors?.descriptionConfig != null && !hasChangedConnectorType} > = ({ singleSelection={SINGLE_SELECTION} data-test-subj="swimlaneDescriptionConfig" onChange={(e) => editMappings('descriptionConfig', e)} - isInvalid={mappingErrors?.descriptionConfig != null} + isInvalid={mappingErrors?.descriptionConfig != null && !hasChangedConnectorType} /> From a83e7d51d23349c0366ba95a6b7347062f5a4b9c Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 3 Jun 2021 13:31:17 +0300 Subject: [PATCH 66/96] Add alert id to params --- .../swimlane/swimlane.tsx | 14 ++++++--- .../swimlane/swimlane_params.tsx | 31 +++++++++++++++++-- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx index 21615ec812132..b25370b098aa9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx @@ -80,17 +80,21 @@ export function getActionType(): ActionTypeModel< validateParams: (actionParams: SwimlaneActionParams): GenericValidationResult => { const errors = { 'subActionParams.incident.ruleName': new Array(), + 'subActionParams.incident.alertId': new Array(), }; const validationResult = { errors, }; - if ( - actionParams.subActionParams && - actionParams.subActionParams.incident && - !actionParams.subActionParams.incident.ruleName?.length - ) { + const hasIncident = actionParams.subActionParams && actionParams.subActionParams.incident; + + if (hasIncident && !actionParams.subActionParams.incident.ruleName?.length) { errors['subActionParams.incident.ruleName'].push(i18n.SW_REQUIRED_RULE_NAME); } + + if (hasIncident && !actionParams.subActionParams.incident.alertId?.length) { + errors['subActionParams.incident.alertId'].push(i18n.SW_REQUIRED_ALERT_ID); + } + return validationResult; }, actionConnectorFields: lazy(() => import('./swimlane_connectors')), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx index d14bdad8accaf..f59ffb04789b5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx @@ -37,14 +37,16 @@ const SwimlaneParamsFields: React.FunctionComponent ({ + hasAlertId: mappings.alertIdConfig != null, hasRuleName: mappings.ruleNameConfig != null, hasAlertSource: mappings.alertSourceConfig != null, hasComments: mappings.commentsConfig != null, hasSeverity: mappings.severityConfig != null, }), [ + mappings.alertIdConfig, mappings.ruleNameConfig, mappings.alertSourceConfig, mappings.commentsConfig, @@ -118,10 +120,35 @@ const SwimlaneParamsFields: React.FunctionComponent + {hasAlertId && ( + <> + 0 && + incident.ruleName !== undefined + } + label={i18n.SW_ALERT_ID_FIELD_LABEL} + > + + + + + )} {hasRuleName && ( <> Date: Thu, 3 Jun 2021 13:43:44 +0300 Subject: [PATCH 67/96] Show mapping if any of the required fields is missing --- .../connectors/swimlane/case_fields.tsx | 19 +++++++++++++++---- .../swimlane/swimlane_params.tsx | 8 ++++++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx index 384677d62f603..1a6a59b7cff2b 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx @@ -13,9 +13,15 @@ import { ConnectorTypes, SwimlaneFieldsType, SwimlaneConnectorType } from '../.. import { ConnectorFieldsProps } from '../types'; import { ConnectorCard } from '../card'; -const casesRequiredFields = ['caseNameConfig', 'descriptionConfig', 'commentsConfig']; -const isMappingEmpty = (mapping: Record | undefined) => - !casesRequiredFields.every((field) => mapping != null && mapping[field] != null); +const casesRequiredFields = [ + 'caseIdConfig', + 'caseNameConfig', + 'descriptionConfig', + 'commentsConfig', +]; + +const isAnyRequiredFieldNotSet = (mapping: Record | undefined) => + !casesRequiredFields.some((field) => mapping != null && mapping[field] != null); const SwimlaneComponent: React.FunctionComponent> = ({ connector, @@ -25,7 +31,12 @@ const SwimlaneComponent: React.FunctionComponent connectorType !== SwimlaneConnectorType.Cases || isMappingEmpty(mappings), + /** + * If the type of the connector is not cases + * or there any of the required fields is not set + * a warning message is being shown to the user + */ + () => connectorType !== SwimlaneConnectorType.Cases || isAnyRequiredFieldNotSet(mappings), [mappings, connectorType] ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx index f59ffb04789b5..8f146fb8d1d37 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx @@ -54,9 +54,13 @@ const SwimlaneParamsFields: React.FunctionComponent { From e21eb4e0f9f99844ea29355deb2ecb93e83ae92c Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 7 Jun 2021 16:43:28 +0300 Subject: [PATCH 68/96] Add tests --- .../builtin_action_types/swimlane/helpers.ts | 6 +- .../swimlane/swimlane.test.tsx | 151 +++++++++++++++++- .../swimlane/translations.ts | 16 +- 3 files changed, 157 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts index 89de49f55fad8..111a4f78433b5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts @@ -48,15 +48,15 @@ export const isValidFieldForConnector = ( }; export const validateMappingForConnector = ( - connector: SwimlaneConnectorType, + connectorType: SwimlaneConnectorType, mapping: SwimlaneMappingConfig ): Record => { - if (connector === SwimlaneConnectorType.All) { + if (connectorType === SwimlaneConnectorType.All || connectorType == null) { return {}; } const requiredFields = - connector === SwimlaneConnectorType.Alerts ? alertsRequiredFields : casesRequiredFields; + connectorType === SwimlaneConnectorType.Alerts ? alertsRequiredFields : casesRequiredFields; return requiredFields.reduce((errors, field) => { if (mapping == null || (mapping != null && mapping[field] == null)) { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx index d205336719275..7a02413d8a43b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx @@ -29,7 +29,7 @@ describe('actionTypeRegistry.get() works', () => { }); describe('swimlane connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { + test('connector validation succeeds when connector is valid', () => { const actionConnector = { secrets: { apiToken: 'test', @@ -40,26 +40,139 @@ describe('swimlane connector validation', () => { config: { apiUrl: 'http:\\test', appId: '1234567asbd32', + connectorType: 'all', mappings: { + alertIdConfig: { id: '1234' }, + alertSourceConfig: { id: '1234' }, + severityConfig: { id: '1234' }, + ruleNameConfig: { id: '1234' }, caseIdConfig: { id: '1234' }, + caseNameConfig: { id: '1234' }, + descriptionConfig: { id: '1234' }, + commentsConfig: { id: '1234' }, }, }, } as SwimlaneActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { errors: { apiUrl: [], appId: [], mappings: [], connectorType: [] } }, + secrets: { errors: { apiToken: [] } }, + }); + }); + + test('it validates correctly when connectorType=all', () => { + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'all', + mappings: {}, + }, + } as SwimlaneActionConnector; + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { apiUrl: [], appId: [], mappings: [], connectorType: [] } }, secrets: { errors: { apiToken: [] } }, }); + }); + + test('it validates correctly when connectorType=cases', () => { + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'cases', + mappings: {}, + }, + } as SwimlaneActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { + errors: { + apiUrl: [], + appId: [], + mappings: [ + { + caseIdConfig: 'Case ID is required.', + caseNameConfig: 'Case name is required.', + commentsConfig: 'Comments are required.', + descriptionConfig: 'Description is required.', + }, + ], + connectorType: [], + }, + }, + secrets: { errors: { apiToken: [] } }, + }); + }); + + test('it validates correctly when connectorType=alerts', () => { + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'alerts', + mappings: {}, + }, + } as SwimlaneActionConnector; - // @ts-ignore - delete actionConnector.config.apiUrl; - actionConnector.secrets.apiToken = 'test1'; expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { - errors: { apiUrl: ['URL is required.'], appId: [], mappings: [], connectorType: [] }, + errors: { + apiUrl: [], + appId: [], + mappings: [ + { + alertIdConfig: 'Alert ID is required.', + ruleNameConfig: 'Rule name is required.', + }, + ], + connectorType: [], + }, }, secrets: { errors: { apiToken: [] } }, }); }); + + test('it validates correctly required config/secrets fields', () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: {}, + } as SwimlaneActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { + errors: { + apiUrl: ['URL is required.'], + appId: ['An App ID is required.'], + mappings: [], + connectorType: [], + }, + }, + secrets: { errors: { apiToken: ['An API token is required.'] } }, + }); + }); }); describe('swimlane action params validation', () => { @@ -67,12 +180,40 @@ describe('swimlane action params validation', () => { const actionParams = { subActionParams: { ruleName: 'Rule Name', + alertId: 'alert-id', + }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + 'subActionParams.incident.ruleName': [], + 'subActionParams.incident.alertId': [], + }, + }); + }); + + test('it validates correctly required fields', () => { + const actionParams = { + subActionParams: { incident: {} }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + 'subActionParams.incident.ruleName': ['Rule name is required.'], + 'subActionParams.incident.alertId': ['Alert ID is required.'], }, + }); + }); + + test('it succeeds when missing incident', () => { + const actionParams = { + subActionParams: {}, }; expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { 'subActionParams.incident.ruleName': [], + 'subActionParams.incident.alertId': [], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts index 5a918a0cdd473..57f477256050b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts @@ -24,14 +24,14 @@ export const SW_ACTION_TYPE_TITLE = i18n.translate( export const SW_REQUIRED_RULE_NAME = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredRuleName', { - defaultMessage: 'Rule Name is required.', + defaultMessage: 'Rule name is required.', } ); export const SW_REQUIRED_APP_ID_TEXT = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAppIdText', { - defaultMessage: 'An App Id is required.', + defaultMessage: 'An App ID is required.', } ); @@ -89,7 +89,7 @@ export const SW_API_URL_INVALID = i18n.translate( export const SW_APP_ID_TEXT_FIELD_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.appIdTextFieldLabel', { - defaultMessage: 'Application Id', + defaultMessage: 'Application ID', } ); @@ -110,7 +110,7 @@ export const SW_MAPPING_TITLE_TEXT_FIELD_LABEL = i18n.translate( export const SW_ALERT_SOURCE_FIELD_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertSourceFieldLabel', { - defaultMessage: 'Alert Source', + defaultMessage: 'Alert source', } ); @@ -131,7 +131,7 @@ export const SW_MAPPING_DESCRIPTION_TEXT_FIELD_LABEL = i18n.translate( export const SW_RULE_NAME_FIELD_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.ruleNameFieldLabel', { - defaultMessage: 'Rule Name', + defaultMessage: 'Rule name', } ); @@ -152,7 +152,7 @@ export const SW_CASE_ID_FIELD_LABEL = i18n.translate( export const SW_CASE_NAME_FIELD_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.caseNameFieldLabel', { - defaultMessage: 'Case Name', + defaultMessage: 'Case name', } ); @@ -226,7 +226,7 @@ export const EMPTY_MAPPING_WARNING_DESC = i18n.translate( export const SW_REQUIRED_ALERT_SOURCE = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertSource', { - defaultMessage: 'Alert Source is required.', + defaultMessage: 'Alert source is required.', } ); @@ -240,7 +240,7 @@ export const SW_REQUIRED_SEVERITY = i18n.translate( export const SW_REQUIRED_CASE_NAME = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredCaseName', { - defaultMessage: 'Case Name is required.', + defaultMessage: 'Case name is required.', } ); From 0b7eeb66774a56b996e90149e1424cda86803b08 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 7 Jun 2021 18:08:55 +0300 Subject: [PATCH 69/96] Fix creation of default mapping --- .../cases/server/client/configure/client.ts | 14 ++------------ .../server/client/configure/create_mappings.ts | 13 +++++-------- .../client/configure/get_default_mappings.ts | 16 ---------------- .../cases/server/client/configure/types.ts | 5 ----- 4 files changed, 7 insertions(+), 41 deletions(-) delete mode 100644 x-pack/plugins/cases/server/client/configure/get_default_mappings.ts diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index 14348e03f99cc..6f7115513db56 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -20,7 +20,6 @@ import { excess, GetConfigureFindRequest, GetConfigureFindRequestRt, - GetFieldsResponse, throwErrors, CasesConfigurationsResponse, CaseConfigurationsResponseRt, @@ -34,7 +33,6 @@ import { } from '../../common'; import { CasesClientInternal } from '../client_internal'; import { CasesClientArgs } from '../types'; -import { getFields } from './get_fields'; import { getMappings } from './get_mappings'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -42,12 +40,7 @@ import { FindActionResult } from '../../../../actions/server/types'; import { ActionType } from '../../../../actions/common'; import { Operations } from '../../authorization'; import { combineAuthorizedAndOwnerFilter } from '../utils'; -import { - ConfigurationGetFields, - MappingsArgs, - CreateMappingsArgs, - UpdateMappingsArgs, -} from './types'; +import { MappingsArgs, CreateMappingsArgs, UpdateMappingsArgs } from './types'; import { createMappings } from './create_mappings'; import { updateMappings } from './update_mappings'; import { @@ -62,7 +55,6 @@ import { * @ignore */ export interface InternalConfigureSubClient { - getFields(params: ConfigurationGetFields): Promise; getMappings( params: MappingsArgs ): Promise['saved_objects']>; @@ -109,10 +101,8 @@ export const createInternalConfigurationSubClient = ( casesClientInternal: CasesClientInternal ): InternalConfigureSubClient => { const configureSubClient: InternalConfigureSubClient = { - getFields: (params: ConfigurationGetFields) => getFields(params, clientArgs), getMappings: (params: MappingsArgs) => getMappings(params, clientArgs), - createMappings: (params: CreateMappingsArgs) => - createMappings(params, clientArgs, casesClientInternal), + createMappings: (params: CreateMappingsArgs) => createMappings(params, clientArgs), updateMappings: (params: UpdateMappingsArgs) => updateMappings(params, clientArgs, casesClientInternal), }; diff --git a/x-pack/plugins/cases/server/client/configure/create_mappings.ts b/x-pack/plugins/cases/server/client/configure/create_mappings.ts index b01f10d7a9e43..c8b0d6f6a6dcd 100644 --- a/x-pack/plugins/cases/server/client/configure/create_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/create_mappings.ts @@ -8,13 +8,13 @@ import { ConnectorMappingsAttributes, ConnectorTypes } from '../../../common/api'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { createCaseError } from '../../common/error'; -import { CasesClientArgs, CasesClientInternal } from '..'; +import { CasesClientArgs } from '..'; import { CreateMappingsArgs } from './types'; +import { createDefaultMapping } from './utils'; export const createMappings = async ( { connectorType, connectorId, owner }: CreateMappingsArgs, - clientArgs: CasesClientArgs, - casesClientInternal: CasesClientInternal + clientArgs: CasesClientArgs ): Promise => { const { unsecuredSavedObjectsClient, connectorMappingsService, logger } = clientArgs; @@ -23,15 +23,12 @@ export const createMappings = async ( return []; } - const res = await casesClientInternal.configuration.getFields({ - connectorId, - connectorType, - }); + const mappings = createDefaultMapping(connectorType); const theMapping = await connectorMappingsService.post({ unsecuredSavedObjectsClient, attributes: { - mappings: res.defaultMappings, + mappings, owner, }, references: [ diff --git a/x-pack/plugins/cases/server/client/configure/get_default_mappings.ts b/x-pack/plugins/cases/server/client/configure/get_default_mappings.ts deleted file mode 100644 index f6051989d6f26..0000000000000 --- a/x-pack/plugins/cases/server/client/configure/get_default_mappings.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { GetDefaultMappingsResponse } from '../../../common'; -import { ConfigureFields } from '../types'; -import { createDefaultMapping } from './utils'; - -export const getDefaultMappings = async ({ - connectorType, -}: ConfigureFields): Promise => { - return createDefaultMapping(connectorType); -}; diff --git a/x-pack/plugins/cases/server/client/configure/types.ts b/x-pack/plugins/cases/server/client/configure/types.ts index a34251690db48..729ee65f76591 100644 --- a/x-pack/plugins/cases/server/client/configure/types.ts +++ b/x-pack/plugins/cases/server/client/configure/types.ts @@ -17,8 +17,3 @@ export interface CreateMappingsArgs extends MappingsArgs { export interface UpdateMappingsArgs extends MappingsArgs { mappingId: string; } - -export interface ConfigurationGetFields { - connectorId: string; - connectorType: string; -} From 6d29a7bc500a7820bc0cf42116b65a48e87b58e4 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 9 Jun 2021 14:57:36 +0300 Subject: [PATCH 70/96] Hide alert id and alert name --- .../builtin_action_types/swimlane/helpers.ts | 9 +-- .../swimlane/swimlane_params.tsx | 58 ++----------------- 2 files changed, 6 insertions(+), 61 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts index 111a4f78433b5..257e46bbe4554 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts @@ -15,13 +15,8 @@ const casesRequiredFields = [ 'caseIdConfig', ]; const casesFields = [...casesRequiredFields]; -const alertsRequiredFields = ['ruleNameConfig', 'alertIdConfig']; -const alertsFields = [ - 'alertSourceConfig', - 'severityConfig', - 'commentsConfig', - ...alertsRequiredFields, -]; +const alertsRequiredFields = ['ruleNameConfig', 'alertIdConfig', 'alertSourceConfig']; +const alertsFields = ['severityConfig', 'commentsConfig', ...alertsRequiredFields]; const translationMapping: Record = { caseIdConfig: i18n.SW_REQUIRED_CASE_ID, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx index 8f146fb8d1d37..d390092b07b28 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx @@ -56,11 +56,11 @@ const SwimlaneParamsFields: React.FunctionComponent { @@ -95,7 +95,7 @@ const SwimlaneParamsFields: React.FunctionComponent - {hasAlertId && ( - <> - 0 && - incident.ruleName !== undefined - } - label={i18n.SW_ALERT_ID_FIELD_LABEL} - > - - - - - )} - {hasRuleName && ( - <> - 0 && - incident.ruleName !== undefined - } - label={i18n.SW_RULE_NAME_FIELD_LABEL} - > - - - - - )} {hasAlertSource && ( <> From 34e08da600240be833572b82d647db90cd837749 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 9 Jun 2021 15:01:53 +0300 Subject: [PATCH 71/96] Async validation --- .../components/builtin_action_types/swimlane/swimlane.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx index b25370b098aa9..8fb43f6dcb8fc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx @@ -32,9 +32,9 @@ export function getActionType(): ActionTypeModel< iconClass: lazy(() => import('./logo')), selectMessage: i18n.SW_SELECT_MESSAGE_TEXT, actionTypeTitle: i18n.SW_ACTION_TYPE_TITLE, - validateConnector: ( + validateConnector: async ( action: SwimlaneActionConnector - ): ConnectorValidationResult => { + ): Promise> => { const configErrors = { apiUrl: new Array(), appId: new Array(), @@ -77,7 +77,9 @@ export function getActionType(): ActionTypeModel< return validationResult; }, - validateParams: (actionParams: SwimlaneActionParams): GenericValidationResult => { + validateParams: async ( + actionParams: SwimlaneActionParams + ): Promise> => { const errors = { 'subActionParams.incident.ruleName': new Array(), 'subActionParams.incident.alertId': new Array(), From d51a24fbd08e53694d2742cad3eb65e41f1bb037 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 9 Jun 2021 15:35:48 +0300 Subject: [PATCH 72/96] Improve field step status --- .../components/builtin_action_types/swimlane/helpers.ts | 2 +- .../builtin_action_types/swimlane/swimlane_connectors.tsx | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts index 257e46bbe4554..91c95c2e5ba7c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts @@ -54,7 +54,7 @@ export const validateMappingForConnector = ( connectorType === SwimlaneConnectorType.Alerts ? alertsRequiredFields : casesRequiredFields; return requiredFields.reduce((errors, field) => { - if (mapping == null || (mapping != null && mapping[field] == null)) { + if (mapping?.[field] == null) { errors = { ...errors, [field]: translationMapping[field] }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx index 53c4c80163077..b6cc81f59dda6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx @@ -58,13 +58,15 @@ const SwimlaneActionConnectorFields: React.FunctionComponent< const editActionConfigCb = useCallback( (k: string, v: string) => { editActionConfig(k, v); - if (k === 'mappings' && Object.keys(v).length === 6) { + if ( + Object.values(errors?.mappings ?? {}).every((mappingError) => mappingError.length === 0) + ) { setStepsStatuses((statuses) => ({ ...statuses, fields: 'complete' })); - } else if (stepsStatuses.fields === 'complete') { + } else { setStepsStatuses((statuses) => ({ ...statuses, fields: 'incomplete' })); } }, - [editActionConfig, stepsStatuses.fields] + [editActionConfig, errors?.mappings] ); return ( From 63a5bb5112119f77e4afa08c608c89e388ca337a Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 9 Jun 2021 16:06:25 +0300 Subject: [PATCH 73/96] Fix tests --- .../builtin_action_types/swimlane/mocks.ts | 7 ++++ .../swimlane/swimlane.test.tsx | 33 ++++++++++--------- .../swimlane/swimlane_connectors.test.tsx | 6 ++-- .../swimlane/swimlane_connectors.tsx | 1 + .../swimlane/swimlane_params.test.tsx | 17 ++++------ 5 files changed, 34 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts index ac1f4dbd21e4b..4ed9d47178a48 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts @@ -6,6 +6,12 @@ */ export const applicationFields = [ + { + id: 'a6ide', + name: 'Alert Id', + key: 'alert-id', + fieldType: 'text', + }, { id: 'adnjls', name: 'Alert Source', @@ -51,6 +57,7 @@ export const applicationFields = [ ]; export const mappings = { + alertIdConfig: applicationFields[0], alertSourceConfig: applicationFields[0], severityConfig: applicationFields[1], ruleNameConfig: applicationFields[2], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx index 7a02413d8a43b..cafe5451e2f98 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx @@ -29,7 +29,7 @@ describe('actionTypeRegistry.get() works', () => { }); describe('swimlane connector validation', () => { - test('connector validation succeeds when connector is valid', () => { + test('connector validation succeeds when connector is valid', async () => { const actionConnector = { secrets: { apiToken: 'test', @@ -54,13 +54,13 @@ describe('swimlane connector validation', () => { }, } as SwimlaneActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { apiUrl: [], appId: [], mappings: [], connectorType: [] } }, secrets: { errors: { apiToken: [] } }, }); }); - test('it validates correctly when connectorType=all', () => { + test('it validates correctly when connectorType=all', async () => { const actionConnector = { secrets: { apiToken: 'test', @@ -76,13 +76,13 @@ describe('swimlane connector validation', () => { }, } as SwimlaneActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { apiUrl: [], appId: [], mappings: [], connectorType: [] } }, secrets: { errors: { apiToken: [] } }, }); }); - test('it validates correctly when connectorType=cases', () => { + test('it validates correctly when connectorType=cases', async () => { const actionConnector = { secrets: { apiToken: 'test', @@ -98,7 +98,7 @@ describe('swimlane connector validation', () => { }, } as SwimlaneActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { apiUrl: [], @@ -118,7 +118,7 @@ describe('swimlane connector validation', () => { }); }); - test('it validates correctly when connectorType=alerts', () => { + test('it validates correctly when connectorType=alerts', async () => { const actionConnector = { secrets: { apiToken: 'test', @@ -134,7 +134,7 @@ describe('swimlane connector validation', () => { }, } as SwimlaneActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { apiUrl: [], @@ -143,6 +143,7 @@ describe('swimlane connector validation', () => { { alertIdConfig: 'Alert ID is required.', ruleNameConfig: 'Rule name is required.', + alertSourceConfig: 'Alert source is required.', }, ], connectorType: [], @@ -152,7 +153,7 @@ describe('swimlane connector validation', () => { }); }); - test('it validates correctly required config/secrets fields', () => { + test('it validates correctly required config/secrets fields', async () => { const actionConnector = { secrets: {}, id: 'test', @@ -161,7 +162,7 @@ describe('swimlane connector validation', () => { config: {}, } as SwimlaneActionConnector; - expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { apiUrl: ['URL is required.'], @@ -176,7 +177,7 @@ describe('swimlane connector validation', () => { }); describe('swimlane 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: { ruleName: 'Rule Name', @@ -184,7 +185,7 @@ describe('swimlane action params validation', () => { }, }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { 'subActionParams.incident.ruleName': [], 'subActionParams.incident.alertId': [], @@ -192,12 +193,12 @@ describe('swimlane action params validation', () => { }); }); - test('it validates correctly required fields', () => { + test('it validates correctly required fields', async () => { const actionParams = { subActionParams: { incident: {} }, }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { 'subActionParams.incident.ruleName': ['Rule name is required.'], 'subActionParams.incident.alertId': ['Alert ID is required.'], @@ -205,12 +206,12 @@ describe('swimlane action params validation', () => { }); }); - test('it succeeds when missing incident', () => { + test('it succeeds when missing incident', async () => { const actionParams = { subActionParams: {}, }; - expect(actionTypeModel.validateParams(actionParams)).toEqual({ + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { 'subActionParams.incident.ruleName': [], 'subActionParams.incident.alertId': [], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx index 1ffea54606e75..ded77a8817fcc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx @@ -36,7 +36,7 @@ describe('SwimlaneActionConnectorFields renders', () => { const wrapper = mountWithIntl( {}} editActionSecrets={() => {}} readOnly={false} @@ -66,7 +66,7 @@ describe('SwimlaneActionConnectorFields renders', () => { const wrapper = mountWithIntl( {}} editActionSecrets={() => {}} readOnly={false} @@ -99,7 +99,7 @@ describe('SwimlaneActionConnectorFields renders', () => { const wrapper = mountWithIntl( {}} editActionSecrets={() => {}} readOnly={false} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx index b6cc81f59dda6..acf9f38e9ba48 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx @@ -68,6 +68,7 @@ const SwimlaneActionConnectorFields: React.FunctionComponent< }, [editActionConfig, errors?.mappings] ); + return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx index 7c11027a071fd..8cbc0a8656416 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx @@ -43,22 +43,17 @@ describe('SwimlaneParamsFields renders', () => { {}} index={0} /> ); - expect(wrapper.find('[data-test-subj="severity"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="comments"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="alertSource"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="ruleName"]').length > 0).toBeTruthy(); + + expect(wrapper.find('[data-test-subj="severity"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="comments"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="alertSource"]').exists()).toBeTruthy(); }); }); From 4b8e2bb083bfdf4afef4317e35630eac613471bb Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 9 Jun 2021 18:15:08 +0300 Subject: [PATCH 74/96] Switch to http server --- .../actions_simulators/server/plugin.ts | 6 +- .../server/swimlane_simulation.ts | 92 ++++++------------- .../actions/builtin_action_types/swimlane.ts | 51 +++++----- 3 files changed, 64 insertions(+), 85 deletions(-) diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts index bd619b9ed9345..a479070c824f2 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts @@ -51,7 +51,6 @@ export function getAllExternalServiceSimulatorPaths(): string[] { allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.JIRA}/rest/api/2/issue`); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.JIRA}/rest/api/2/createmeta`); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.RESILIENT}/rest/orgs/201/incidents`); - allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SWIMLANE}/api`); return allPaths; } @@ -69,6 +68,10 @@ export async function getSlackServer(): Promise { return await initSlack(); } +export async function getSwimlaneServer(): Promise { + return await initSwimlane(); +} + interface FixtureSetupDeps { actions: ActionsPluginSetupContract; features: FeaturesPluginSetup; @@ -120,7 +123,6 @@ export class FixturePlugin implements Plugin, - res: KibanaResponseFactory - ): Promise> { - return jsonResponse(res, 200, { - id: 'wowzeronza', - name: 'ET-69', - createdDate: '2021-06-01T17:29:51.092Z', - }); - } - ); - router.patch( - { - path: `${path}/api/app/{id}/record/{recordId}`, - options: { - authRequired: false, - }, - validate: {}, - }, - // Swimlane simulator: create an action pointing here, and you can get - // different responses based on the message posted. See the README.md for - // more info. - async function ( - context: RequestHandlerContext, - req: KibanaRequest, - res: KibanaResponseFactory - ): Promise> { - return jsonResponse(res, 200, { - id: 'wowzeronza', - name: 'ET-69', - modifiedDate: '2021-06-01T17:29:51.092Z', - }); - } - ); -} +export const initPlugin = async () => http.createServer(handler); -function jsonResponse( - res: KibanaResponseFactory, - code: number, - object: Record = {} -) { - return res.custom>({ body: object, statusCode: code }); -} +const sendResponse = (response: http.ServerResponse, data: any) => { + response.statusCode = 200; + response.setHeader('Content-Type', 'application/json'); + response.end(JSON.stringify(data, null, 4)); +}; + +const handler = (request: http.IncomingMessage, response: http.ServerResponse) => { + if (request.method === 'POST') { + return sendResponse(response, { + id: 'wowzeronza', + name: 'ET-69', + createdDate: '2021-06-01T17:29:51.092Z', + }); + } + + if (request.method === 'PATCH') { + return sendResponse(response, { + id: 'wowzeronza', + name: 'ET-69', + modifiedDate: '2021-06-01T17:29:51.092Z', + }); + } + + // Return an 400 error if http method is not supported + response.statusCode = 400; + response.setHeader('Content-Type', 'application/json'); + response.end('Not supported http method to request slack simulator'); +}; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts index aef8b9d75a5a7..f20c52c64640d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts @@ -7,14 +7,12 @@ import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; +import getPort from 'get-port'; +import http from 'http'; import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -import { - getExternalServiceSimulatorPath, - ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getSwimlaneServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function swimlaneTest({ getService }: FtrProviderContext) { @@ -30,6 +28,12 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { appId: '123456asdf', connectorType: 'all', mappings: { + alertIdConfig: { + id: 'ednjls', + name: 'Alert id', + key: 'alert-id', + fieldType: 'text', + }, alertSourceConfig: { id: 'adnjls', name: 'Alert Source', @@ -81,6 +85,7 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { subAction: 'pushToService', subActionParams: { incident: { + alertId: 'fs345f78g', ruleName: 'Rule Name', severity: 'Critical', alertSource: 'Elastic', @@ -100,11 +105,26 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { }; describe('Swimlane', () => { - let swimlaneSimulatorURL: string = ''; + let simulatedActionId = ''; + let swimlaneSimulatorURL: string = ''; + let swimlaneServer: http.Server; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; + // need to wait for kibanaServer to settle ... before(async () => { - swimlaneSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SWIMLANE) + swimlaneServer = await getSwimlaneServer(); + const availablePort = await getPort({ port: getPort.makeRange(9000, 9100) }); + if (!swimlaneServer.listening) { + swimlaneServer.listen(availablePort); + } + swimlaneSimulatorURL = `http://localhost:${availablePort}`; + proxyServer = await getHttpProxyServer( + swimlaneSimulatorURL, + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } ); }); @@ -249,9 +269,6 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { }); describe('Swimlane - Executor', () => { - let simulatedActionId: string; - let proxyServer: httpProxy | undefined; - let proxyHaveBeenCalled = false; before(async () => { const { body } = await supertest .post('/api/actions/connector') @@ -266,14 +283,6 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { secrets: mockSwimlane.secrets, }); simulatedActionId = body.id; - - proxyServer = await getHttpProxyServer( - kibanaServer.resolveUrl('/'), - configService.get('kbnTestServer.serverArgs'), - () => { - proxyHaveBeenCalled = true; - } - ); }); describe('Validation', () => { @@ -380,7 +389,7 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [subActionParams.incident.comments]: definition for this key is missing', + 'error validating action params: [subActionParams.comments]: types that failed validation:\n- [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n- [subActionParams.comments.1]: expected value to equal [null]', }); }); }); @@ -404,7 +413,7 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [subActionParams.incident.comments]: definition for this key is missing', + 'error validating action params: [subActionParams.comments]: types that failed validation:\n- [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n- [subActionParams.comments.1]: expected value to equal [null]', }); }); }); From 3eb39e9eeb2fd40079ec79781612d36280948091 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 10 Jun 2021 15:16:04 +0300 Subject: [PATCH 75/96] Switch to new mapping --- .../cases/server/connectors/factory.ts | 4 ++- .../server/connectors/swimlane/format.test.ts | 21 ++++++++++++++ ...xternal_service_formatter.ts => format.ts} | 10 ++----- .../cases/server/connectors/swimlane/index.ts | 15 ++++++++++ .../server/connectors/swimlane/mapping.ts | 28 +++++++++++++++++++ .../cases/server/connectors/swimlane/types.ts | 13 +++++++++ 6 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/cases/server/connectors/swimlane/format.test.ts rename x-pack/plugins/cases/server/connectors/swimlane/{external_service_formatter.ts => format.ts} (54%) create mode 100644 x-pack/plugins/cases/server/connectors/swimlane/index.ts create mode 100644 x-pack/plugins/cases/server/connectors/swimlane/mapping.ts create mode 100644 x-pack/plugins/cases/server/connectors/swimlane/types.ts diff --git a/x-pack/plugins/cases/server/connectors/factory.ts b/x-pack/plugins/cases/server/connectors/factory.ts index 64e3e6f3eb225..40a6702f11b0f 100644 --- a/x-pack/plugins/cases/server/connectors/factory.ts +++ b/x-pack/plugins/cases/server/connectors/factory.ts @@ -6,16 +6,18 @@ */ import { ConnectorTypes } from '../../common/api'; +import { ICasesConnector, CasesConnectorsMap } from './types'; import { getCaseConnector as getJiraCaseConnector } from './jira'; import { getCaseConnector as getResilientCaseConnector } from './resilient'; import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow'; -import { ICasesConnector, CasesConnectorsMap } from './types'; +import { getCaseConnector as getSwimlaneCaseConnector } from './swimlane'; const mapping: Record = { [ConnectorTypes.jira]: getJiraCaseConnector(), [ConnectorTypes.serviceNowITSM]: getServiceNowITSMCaseConnector(), [ConnectorTypes.serviceNowSIR]: getServiceNowSIRCaseConnector(), [ConnectorTypes.resilient]: getResilientCaseConnector(), + [ConnectorTypes.swimlane]: getSwimlaneCaseConnector(), [ConnectorTypes.none]: null, }; diff --git a/x-pack/plugins/cases/server/connectors/swimlane/format.test.ts b/x-pack/plugins/cases/server/connectors/swimlane/format.test.ts new file mode 100644 index 0000000000000..55cbbdb68691e --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/swimlane/format.test.ts @@ -0,0 +1,21 @@ +/* + * 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 { CaseResponse } from '../../../common'; +import { format } from './format'; + +describe('Swimlane formatter', () => { + const theCase = { + id: 'case-id', + connector: { fields: null }, + } as CaseResponse; + + it('it formats correctly', async () => { + const res = await format(theCase, []); + expect(res).toEqual({ caseId: theCase.id }); + }); +}); diff --git a/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts b/x-pack/plugins/cases/server/connectors/swimlane/format.ts similarity index 54% rename from x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts rename to x-pack/plugins/cases/server/connectors/swimlane/format.ts index 9d25b8fb663cf..9531e4099a4f4 100644 --- a/x-pack/plugins/cases/server/connectors/swimlane/external_service_formatter.ts +++ b/x-pack/plugins/cases/server/connectors/swimlane/format.ts @@ -5,15 +5,11 @@ * 2.0. */ -import { ExternalServiceFormatter } from '../types'; -import { ConnectorSwimlaneTypeFields, SwimlaneFieldsType } from '../../../common'; +import { ConnectorSwimlaneTypeFields } from '../../../common'; +import { Format } from './types'; -const format: ExternalServiceFormatter['format'] = (theCase) => { +export const format: Format = (theCase) => { const { caseId = theCase.id } = (theCase.connector.fields as ConnectorSwimlaneTypeFields['fields']) ?? {}; return { caseId }; }; - -export const swimlaneExternalServiceFormatter: ExternalServiceFormatter = { - format, -}; diff --git a/x-pack/plugins/cases/server/connectors/swimlane/index.ts b/x-pack/plugins/cases/server/connectors/swimlane/index.ts new file mode 100644 index 0000000000000..2cad92391bdec --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/swimlane/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { getMapping } from './mapping'; +import { format } from './format'; +import { SwimlaneCaseConnector } from './types'; + +export const getCaseConnector = (): SwimlaneCaseConnector => ({ + getMapping, + format, +}); diff --git a/x-pack/plugins/cases/server/connectors/swimlane/mapping.ts b/x-pack/plugins/cases/server/connectors/swimlane/mapping.ts new file mode 100644 index 0000000000000..e1e34054463e5 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/swimlane/mapping.ts @@ -0,0 +1,28 @@ +/* + * 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 { GetMapping } from './types'; + +export const getMapping: GetMapping = () => { + return [ + { + source: 'title', + target: 'caseName', + action_type: 'overwrite', + }, + { + source: 'description', + target: 'description', + action_type: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + action_type: 'append', + }, + ]; +}; diff --git a/x-pack/plugins/cases/server/connectors/swimlane/types.ts b/x-pack/plugins/cases/server/connectors/swimlane/types.ts new file mode 100644 index 0000000000000..22a1e9f6372d5 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/swimlane/types.ts @@ -0,0 +1,13 @@ +/* + * 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 { SwimlaneFieldsType } from '../../../common/api'; +import { ICasesConnector } from '../types'; + +export type SwimlaneCaseConnector = ICasesConnector; +export type Format = ICasesConnector['format']; +export type GetMapping = ICasesConnector['getMapping']; From 36aa3b8f76de7ce1246ba97c07d142b167fd1f34 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 15 Jun 2021 09:19:53 +0300 Subject: [PATCH 76/96] Improve callout message --- .../public/components/connectors/swimlane/translations.ts | 5 +++-- .../components/builtin_action_types/swimlane/swimlane.tsx | 1 + .../components/builtin_action_types/swimlane/translations.ts | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts index 0428b58080099..edbc19dde64a6 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts @@ -29,13 +29,14 @@ export const SEVERITY_LABEL = i18n.translate('xpack.cases.connectors.swimlane.se export const EMPTY_MAPPING_WARNING_TITLE = i18n.translate( 'xpack.cases.connectors.swimlane.emptyMappingWarningTitle', { - defaultMessage: 'Empty mapping', + defaultMessage: 'This connector has missing field mappings', } ); export const EMPTY_MAPPING_WARNING_DESC = i18n.translate( 'xpack.cases.connectors.swimlane.emptyMappingWarningDesc', { - defaultMessage: `The mapping of the connector is empty. Create a connector of type Cases before pushing to a case.`, + defaultMessage: + 'This connector cannot be selected because it is missing the required case field mappings. You edit this connector to add required field mappings or select a connector of type Cases.', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx index 8fb43f6dcb8fc..5e06e3935eebd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx @@ -87,6 +87,7 @@ export function getActionType(): ActionTypeModel< const validationResult = { errors, }; + const hasIncident = actionParams.subActionParams && actionParams.subActionParams.incident; if (hasIncident && !actionParams.subActionParams.incident.ruleName?.length) { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts index 57f477256050b..41681941c36dd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts @@ -212,14 +212,15 @@ export const SW_FIELD_MAPPING_IS_REQUIRED = i18n.translate( export const EMPTY_MAPPING_WARNING_TITLE = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningTitle', { - defaultMessage: 'Empty mapping', + defaultMessage: 'This connector has missing field mappings', } ); export const EMPTY_MAPPING_WARNING_DESC = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningDesc', { - defaultMessage: `The mapping of the connector is empty. Create a connector of type Alerts before pushing to a case.`, + defaultMessage: + 'This connector cannot be selected because it is missing the required case field mappings. You edit this connector to add required field mappings or select a connector of type Alerts.', } ); From f4771dee72c4f0e213c7a013cb49880ddcf63c55 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 15 Jun 2021 10:54:03 +0300 Subject: [PATCH 77/96] Add help text to alert source --- .../cases/public/common/shared_imports.ts | 2 + .../public/components/case_view/index.tsx | 7 +-- .../components/configure_cases/index.tsx | 8 +-- .../components/configure_cases/utils.ts | 13 ++--- .../components/connectors/fields_form.tsx | 3 +- .../connectors/swimlane/case_fields.tsx | 13 +---- .../connectors/swimlane/validator.ts | 33 ++++++++++++ .../public/components/connectors/types.ts | 3 +- .../public/components/create/connector.tsx | 50 +++++++++++++++++-- .../public/components/create/form_context.tsx | 33 ++---------- .../components/edit_connector/index.tsx | 10 +++- .../plugins/cases/public/components/types.ts | 10 ++++ .../plugins/cases/public/components/utils.ts | 35 +++++++++++++ .../swimlane/swimlane.tsx | 5 ++ .../swimlane/swimlane_params.tsx | 32 ++++++++++-- .../swimlane/translations.ts | 8 +++ .../actions/builtin_action_types/swimlane.ts | 2 - 17 files changed, 192 insertions(+), 75 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts create mode 100644 x-pack/plugins/cases/public/components/types.ts create mode 100644 x-pack/plugins/cases/public/components/utils.ts diff --git a/x-pack/plugins/cases/public/common/shared_imports.ts b/x-pack/plugins/cases/public/common/shared_imports.ts index 675204076b02a..4641fcfa2167c 100644 --- a/x-pack/plugins/cases/public/common/shared_imports.ts +++ b/x-pack/plugins/cases/public/common/shared_imports.ts @@ -24,6 +24,8 @@ export { ValidationError, ValidationFunc, VALIDATION_TYPES, + FieldConfig, + ValidationConfig, } from '../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { Field, diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index 86b13ae5a863c..08563fb1b911e 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -32,11 +32,7 @@ import { useGetCaseUserActions } from '../../containers/use_get_case_user_action import { usePushToService } from '../use_push_to_service'; import { EditConnector } from '../edit_connector'; import { useConnectors } from '../../containers/configure/use_connectors'; -import { - getConnectorById, - normalizeActionConnector, - getNoneConnector, -} from '../configure_cases/utils'; +import { normalizeActionConnector, getNoneConnector } from '../configure_cases/utils'; import { StatusActionButton } from '../status/button'; import * as i18n from './translations'; import { Ecs } from '../../../common'; @@ -44,6 +40,7 @@ import { CasesTimelineIntegration, CasesTimelineIntegrationProvider } from '../t import { useTimelineContext } from '../timeline_context/use_timeline_context'; import { CasesNavigation } from '../links'; import { OwnerProvider } from '../owner_context'; +import { getConnectorById } from '../utils'; const gutterTimeline = '70px'; // seems to be a timeline reference from the original file export interface CaseViewComponentProps { diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx index 3ee4bc77cd237..ac43ec05319a0 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx @@ -24,15 +24,11 @@ import { ActionConnectorTableItem } from '../../../../triggers_actions_ui/public import { SectionWrapper } from '../wrappers'; import { Connectors } from './connectors'; import { ClosureOptions } from './closure_options'; -import { - getConnectorById, - getNoneConnector, - normalizeActionConnector, - normalizeCaseConnector, -} from './utils'; +import { getNoneConnector, normalizeActionConnector, normalizeCaseConnector } from './utils'; import * as i18n from './translations'; import { Owner } from '../../types'; import { OwnerProvider } from '../owner_context'; +import { getConnectorById } from '../utils'; const FormWrapper = styled.div` ${({ theme }) => css` diff --git a/x-pack/plugins/cases/public/components/configure_cases/utils.ts b/x-pack/plugins/cases/public/components/configure_cases/utils.ts index ade1a5e0c2bba..6597417b5068a 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/utils.ts +++ b/x-pack/plugins/cases/public/components/configure_cases/utils.ts @@ -10,10 +10,10 @@ import { CaseField, ActionType, ThirdPartyField, - ActionConnector, CaseConnector, CaseConnectorMapping, } from '../../containers/configure/types'; +import { CaseActionConnector } from '../types'; export const setActionTypeToMapping = ( caseField: CaseField, @@ -54,13 +54,8 @@ export const getNoneConnector = (): CaseConnector => ({ fields: null, }); -export const getConnectorById = ( - id: string, - connectors: ActionConnector[] -): ActionConnector | null => connectors.find((c) => c.id === id) ?? null; - export const normalizeActionConnector = ( - actionConnector: ActionConnector, + actionConnector: CaseActionConnector, fields: CaseConnector['fields'] = null ): CaseConnector => { const caseConnectorFieldsType = { @@ -75,6 +70,6 @@ export const normalizeActionConnector = ( }; export const normalizeCaseConnector = ( - connectors: ActionConnector[], + connectors: CaseActionConnector[], caseConnector: CaseConnector -): ActionConnector | null => connectors.find((c) => c.id === caseConnector.id) ?? null; +): CaseActionConnector | null => connectors.find((c) => c.id === caseConnector.id) ?? null; diff --git a/x-pack/plugins/cases/public/components/connectors/fields_form.tsx b/x-pack/plugins/cases/public/components/connectors/fields_form.tsx index d71da6f87689d..062695fa41cc2 100644 --- a/x-pack/plugins/cases/public/components/connectors/fields_form.tsx +++ b/x-pack/plugins/cases/public/components/connectors/fields_form.tsx @@ -8,7 +8,8 @@ import React, { memo, Suspense } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; -import { CaseActionConnector, ConnectorFieldsProps } from './types'; +import { CaseActionConnector } from '../types'; +import { ConnectorFieldsProps } from './types'; import { getCaseConnectors } from '.'; import { ConnectorTypeFields } from '../../../common'; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx index 1a6a59b7cff2b..2f15baeb2913f 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx @@ -12,16 +12,7 @@ import * as i18n from './translations'; import { ConnectorTypes, SwimlaneFieldsType, SwimlaneConnectorType } from '../../../../common'; import { ConnectorFieldsProps } from '../types'; import { ConnectorCard } from '../card'; - -const casesRequiredFields = [ - 'caseIdConfig', - 'caseNameConfig', - 'descriptionConfig', - 'commentsConfig', -]; - -const isAnyRequiredFieldNotSet = (mapping: Record | undefined) => - !casesRequiredFields.some((field) => mapping != null && mapping[field] != null); +import { isAnyRequiredFieldNotSet } from './validator'; const SwimlaneComponent: React.FunctionComponent> = ({ connector, @@ -51,7 +42,7 @@ const SwimlaneComponent: React.FunctionComponent )} {showMappingWarning && ( - + {i18n.EMPTY_MAPPING_WARNING_DESC} )} diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts new file mode 100644 index 0000000000000..95d96df9daf1e --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SwimlaneConnectorType } from '../../../../common'; +import { ValidationConfig } from '../../../common/shared_imports'; +import { CaseActionConnector } from '../../types'; + +const casesRequiredFields = [ + 'caseIdConfig', + 'caseNameConfig', + 'descriptionConfig', + 'commentsConfig', +]; + +export const isAnyRequiredFieldNotSet = (mapping: Record | undefined) => + !casesRequiredFields.some((field) => mapping != null && mapping[field] != null); + +export const connectorValidator = ( + connector: CaseActionConnector +): ReturnType => { + const { + config: { mappings, connectorType }, + } = connector; + if (connectorType !== SwimlaneConnectorType.Cases || isAnyRequiredFieldNotSet(mappings)) { + return { + message: 'Invalid connector', + }; + } +}; diff --git a/x-pack/plugins/cases/public/components/connectors/types.ts b/x-pack/plugins/cases/public/components/connectors/types.ts index 4eb97513b9f58..5bbd77c790901 100644 --- a/x-pack/plugins/cases/public/components/connectors/types.ts +++ b/x-pack/plugins/cases/public/components/connectors/types.ts @@ -11,12 +11,11 @@ import React from 'react'; import { ActionType as ThirdPartySupportedActions, CaseField, - ActionConnector, ConnectorTypeFields, } from '../../../common'; +import { CaseActionConnector } from '../types'; export { ThirdPartyField as AllThirdPartyFields } from '../../../common'; -export type CaseActionConnector = ActionConnector; export interface ThirdPartyField { label: string; diff --git a/x-pack/plugins/cases/public/components/create/connector.tsx b/x-pack/plugins/cases/public/components/create/connector.tsx index 9591933806946..3aad3d7610fcc 100644 --- a/x-pack/plugins/cases/public/components/create/connector.tsx +++ b/x-pack/plugins/cases/public/components/create/connector.tsx @@ -5,16 +5,23 @@ * 2.0. */ -import React, { memo, useCallback } from 'react'; +import React, { memo, useCallback, useMemo, useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { ConnectorTypes } from '../../../common'; -import { UseField, useFormData, FieldHook, useFormContext } from '../../common/shared_imports'; +import { + UseField, + useFormData, + FieldHook, + useFormContext, + FieldConfig, +} from '../../common/shared_imports'; import { ConnectorSelector } from '../connector_selector/form'; import { ConnectorFieldsForm } from '../connectors/fields_form'; import { ActionConnector } from '../../../common'; -import { getConnectorById } from '../configure_cases/utils'; -import { FormProps } from './schema'; +import { FormProps, schema } from './schema'; +import { useCaseConfigure } from '../../containers/configure/use_configure'; +import { getConnectorById, getConnectorsFormValidators } from '../utils'; interface Props { connectors: ActionConnector[]; @@ -27,6 +34,7 @@ interface ConnectorsFieldProps { connectors: ActionConnector[]; field: FieldHook; isEdit: boolean; + setErrors: (errors: boolean) => void; hideConnectorServiceNowSir?: boolean; } @@ -34,6 +42,7 @@ const ConnectorFields = ({ connectors, isEdit, field, + setErrors, hideConnectorServiceNowSir = false, }: ConnectorsFieldProps) => { const [{ connectorId }] = useFormData({ watch: ['connectorId'] }); @@ -62,18 +71,49 @@ const ConnectorComponent: React.FC = ({ isLoading, isLoadingConnectors, }) => { - const { getFields } = useFormContext(); + const { getFields, setFieldValue } = useFormContext(); + const { connector: configurationConnector } = useCaseConfigure(); + const handleConnectorChange = useCallback(() => { const { fields } = getFields(); fields.setValue(null); }, [getFields]); + const defaultConnectorId = useMemo(() => { + if ( + hideConnectorServiceNowSir && + configurationConnector.type === ConnectorTypes.serviceNowSIR + ) { + return 'none'; + } + return connectors.some((connector) => connector.id === configurationConnector.id) + ? configurationConnector.id + : 'none'; + }, [ + configurationConnector.id, + configurationConnector.type, + connectors, + hideConnectorServiceNowSir, + ]); + + useEffect(() => setFieldValue('connectorId', defaultConnectorId), [ + defaultConnectorId, + setFieldValue, + ]); + + const connectorIdConfig = getConnectorsFormValidators({ + config: schema.connectorId as FieldConfig, + connectors, + }); + return ( = ({ }) => { const { connectors, loading: isLoadingConnectors } = useConnectors(); const owner = useOwnerContext(); - const { connector: configurationConnector } = useCaseConfigure(); const { postCase } = usePostCase(); const { postComment } = usePostComment(); const { pushCaseToExternalService } = usePostPushToService(); - const connectorId = useMemo(() => { - if ( - hideConnectorServiceNowSir && - configurationConnector.type === ConnectorTypes.serviceNowSIR - ) { - return 'none'; - } - return connectors.some((connector) => connector.id === configurationConnector.id) - ? configurationConnector.id - : 'none'; - }, [ - configurationConnector.id, - configurationConnector.type, - connectors, - hideConnectorServiceNowSir, - ]); - const submitCase = useCallback( async ( { connectorId: dataConnectorId, fields, syncAlerts, ...dataWithoutConnectorId }, @@ -125,9 +103,6 @@ export const FormContext: React.FC = ({ schema, onSubmit: submitCase, }); - const { setFieldValue } = form; - // Set the selected connector to the configuration connector - useEffect(() => setFieldValue('connectorId', connectorId), [connectorId, setFieldValue]); const childrenWithExtraProp = useMemo( () => diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.tsx index ad6b5a5e7cddf..8f5f75a7db1d9 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.tsx @@ -20,15 +20,15 @@ import { import styled from 'styled-components'; import { noop } from 'lodash/fp'; -import { Form, UseField, useForm } from '../../common/shared_imports'; +import { FieldConfig, Form, UseField, useForm } from '../../common/shared_imports'; import { ActionConnector, ConnectorTypeFields } from '../../../common'; import { ConnectorSelector } from '../connector_selector/form'; import { ConnectorFieldsForm } from '../connectors/fields_form'; -import { getConnectorById } from '../configure_cases/utils'; import { CaseUserActions } from '../../containers/types'; import { schema } from './schema'; import { getConnectorFieldsFromUserActions } from './helpers'; import * as i18n from './translations'; +import { getConnectorById, getConnectorsFormValidators } from '../utils'; interface EditConnectorProps { caseFields: ConnectorTypeFields['fields']; @@ -203,6 +203,11 @@ export const EditConnector = React.memo( }); }, [dispatch]); + const connectorIdConfig = getConnectorsFormValidators({ + config: schema.connectorId as FieldConfig, + connectors, + }); + return ( @@ -230,6 +235,7 @@ export const EditConnector = React.memo( connectors.find((c) => c.id === id) ?? null; + +export const getConnectorsFormValidators = ({ + connectors = [], + config = {}, +}: { + connectors: CaseActionConnector[]; + config: FieldConfig; +}): FieldConfig => ({ + ...config, + validations: [ + { + validator: ({ value: connectorId }) => { + const connector = getConnectorById(connectorId as string, connectors); + if (connector != null) { + return swimlaneConnectorValidator(connector); + } + }, + }, + ], +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx index 5e06e3935eebd..c96fd5737ca4a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx @@ -83,6 +83,7 @@ export function getActionType(): ActionTypeModel< const errors = { 'subActionParams.incident.ruleName': new Array(), 'subActionParams.incident.alertId': new Array(), + 'subActionParams.incident.alertSource': new Array(), }; const validationResult = { errors, @@ -98,6 +99,10 @@ export function getActionType(): ActionTypeModel< errors['subActionParams.incident.alertId'].push(i18n.SW_REQUIRED_ALERT_ID); } + if (hasIncident && !actionParams.subActionParams.incident.alertSource?.length) { + errors['subActionParams.incident.alertSource'].push(i18n.SW_REQUIRED_ALERT_SOURCE); + } + return validationResult; }, actionConnectorFields: lazy(() => import('./swimlane_connectors')), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx index d390092b07b28..50a6be9826843 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx @@ -5,14 +5,30 @@ * 2.0. */ -import React, { useCallback, useEffect, useRef, useMemo } from 'react'; -import { EuiCallOut, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import React, { memo, useCallback, useEffect, useRef, useMemo } from 'react'; +import { + EuiCallOut, + EuiFormRow, + EuiIconTip, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; import * as i18n from './translations'; import { ActionParamsProps } from '../../../../types'; import { SwimlaneActionConnector, SwimlaneActionParams, SwimlaneConnectorType } from './types'; import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; +const AlertSourceLabel = memo(() => ( + + {i18n.SW_ALERT_SOURCE_FIELD_LABEL} + + + + +)); + const SwimlaneParamsFields: React.FunctionComponent> = ({ actionParams, editAction, @@ -126,7 +142,17 @@ const SwimlaneParamsFields: React.FunctionComponent {hasAlertSource && ( <> - + } + error={errors['subActionParams.incident.alertSource'] as string[]} + isInvalid={ + errors['subActionParams.incident.alertSource'] !== undefined && + errors['subActionParams.incident.alertSource'].length > 0 && + incident.alertSource !== undefined + } + > { swimlaneServer = await getSwimlaneServer(); const availablePort = await getPort({ port: getPort.makeRange(9000, 9100) }); From 762c095cebec86cd6a297d8655e36a56b35b81e4 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 15 Jun 2021 19:53:09 +0300 Subject: [PATCH 78/96] Improve helpers --- .../builtin_action_types/jira/service.test.ts | 12 +- .../swimlane/helpers.test.ts | 51 +++- .../builtin_action_types/swimlane/helpers.ts | 58 ++--- .../swimlane/service.test.ts | 225 +++++++++++++++++- .../builtin_action_types/swimlane/service.ts | 6 +- .../builtin_action_types/swimlane/types.ts | 6 +- 6 files changed, 292 insertions(+), 66 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index f6462bac9d83e..9430d734287d3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -155,12 +155,12 @@ describe('Jira service', () => { ).toThrow(); }); - test('throws without username', () => { + test('throws without email/username', () => { expect(() => createExternalService( { - config: { apiUrl: 'test.com' }, - secrets: { apiToken: '', email: 'elastic@elastic.com' }, + config: { apiUrl: 'test.com', projectKey: 'CK' }, + secrets: { apiToken: 'token' }, }, logger, configurationUtilities @@ -168,12 +168,12 @@ describe('Jira service', () => { ).toThrow(); }); - test('throws without password', () => { + test('throws without apiToken/password', () => { expect(() => createExternalService( { - config: { apiUrl: 'test.com' }, - secrets: { apiToken: '', email: undefined }, + config: { apiUrl: 'test.com', projectKey: 'CK' }, + secrets: { email: 'elastic@elastic.com' }, }, logger, configurationUtilities diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts index 4657c42b1e37b..3ba696f973f1f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts @@ -11,7 +11,7 @@ import { mappings } from './mocks'; describe('Create Record Mapping', () => { const appId = '45678'; - test('Mapping is Successful', () => { + test('it maps successfully', () => { const params = { alertId: 'al123', ruleName: 'Rule Name', @@ -19,17 +19,50 @@ describe('Create Record Mapping', () => { alertSource: 'Elastic', caseName: 'Case Name', caseId: 'es3456789', - comments: 'This is a comment', description: 'case desc', externalId: null, }; + const data = getBodyForEventAction(appId, mappings, params); - expect(data?.values?.[mappings.alertSourceConfig?.id ?? 0]).toEqual(params.alertSource); - expect(data?.values?.[mappings.ruleNameConfig.id]).toEqual(params.ruleName); - expect(data?.values?.[mappings.caseNameConfig?.id ?? 0]).toEqual(params.caseName); - expect(data?.values?.[mappings.caseIdConfig?.id ?? 0]).toEqual(params.caseId); - expect(data?.values?.[mappings.commentsConfig?.id ?? 0]).toEqual(params.comments); - expect(data?.values?.[mappings?.severityConfig?.id ?? 0]).toEqual(params.severity); - expect(data?.values?.[mappings?.descriptionConfig?.id ?? 0]).toEqual(params.description); + expect(data.applicationId).toEqual(appId); + expect(data.id).not.toBeDefined(); + expect(data.values?.[mappings.alertSourceConfig?.id ?? 0]).toEqual(params.alertSource); + expect(data.values?.[mappings.ruleNameConfig.id]).toEqual(params.ruleName); + expect(data.values?.[mappings.caseNameConfig?.id ?? 0]).toEqual(params.caseName); + expect(data.values?.[mappings.caseIdConfig?.id ?? 0]).toEqual(params.caseId); + expect(data.values?.[mappings?.severityConfig?.id ?? 0]).toEqual(params.severity); + expect(data.values?.[mappings?.descriptionConfig?.id ?? 0]).toEqual(params.description); + }); + + test('it contains the id if defined', () => { + const params = { + alertId: 'al123', + ruleName: 'Rule Name', + severity: 'Critical', + alertSource: 'Elastic', + caseName: 'Case Name', + caseId: 'es3456789', + description: 'case desc', + externalId: null, + }; + const data = getBodyForEventAction(appId, mappings, params, '123'); + expect(data.id).toEqual('123'); + }); + + test('it does not includes null mappings', () => { + const params = { + alertId: 'al123', + ruleName: 'Rule Name', + severity: 'Critical', + alertSource: 'Elastic', + caseName: 'Case Name', + caseId: 'es3456789', + description: 'case desc', + externalId: null, + }; + + // @ts-expect-error + const data = getBodyForEventAction(appId, { ...mappings, test: null }, params); + expect(data.values?.test).not.toBeDefined(); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts index 36f9b6b711c93..dc18f0a29e6d1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts @@ -5,13 +5,21 @@ * 2.0. */ -import { - CreateRecordParams, - MappingConfigType, - SwimlaneDataComments, - SwimlaneDataValues, - SwimlaneRecordPayload, -} from './types'; +import { TypeOf } from '@kbn/config-schema'; +import { ConfigMappingSchema } from './schema'; +import { CreateRecordParams, Incident, MappingConfigType, SwimlaneRecordPayload } from './types'; + +type ConfigMapping = Omit, 'commentsConfig'>; + +const mappingKeysToIncidentKeys: Record = { + ruleNameConfig: 'ruleName', + alertIdConfig: 'alertId', + alertSourceConfig: 'alertSource', + caseIdConfig: 'caseId', + caseNameConfig: 'caseName', + severityConfig: 'severity', + descriptionConfig: 'description', +}; export const getBodyForEventAction = ( applicationId: string, @@ -22,50 +30,32 @@ export const getBodyForEventAction = ( const data: SwimlaneRecordPayload = { applicationId, ...(incidentId ? { id: incidentId } : {}), + values: {}, }; - const values: SwimlaneDataValues = {}; - const comments: SwimlaneDataComments = {}; - - for (const mappingsKey of Object.keys(mappingConfig)) { - const fieldMap = mappingConfig[mappingsKey]; + return (Object.keys(mappingConfig) as Array).reduce((acc, key) => { + const fieldMap = mappingConfig[key]; if (!fieldMap) { - continue; + return acc; } - const createdDate = new Date().toISOString(); const { id, fieldType } = fieldMap; - const paramName = mappingsKey.replace('Config', '') as keyof CreateRecordParams['incident']; - + const paramName = mappingKeysToIncidentKeys[key]; const value = params[paramName]; if (value) { switch (fieldType) { - case 'comments': { - comments[id] = [ - ...(comments[id] != null ? comments[id] : []), - { fieldId: id, message: value, createdDate, isRichText: true }, - ]; - break; - } case 'numeric': { const number = Number(value); - values[id] = isNaN(number) ? 0 : number; - break; + return { ...acc, values: { ...acc.values, [id]: isNaN(number) ? 0 : number } }; } default: { - values[id] = value; - break; + return { ...acc, values: { ...acc.values, [id]: value } }; } } } - } - - data.values = values; - if (Object.keys(comments).length) { - data.comments = comments; - } - return data; + return acc; + }, data); }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts index fd77144219d14..3fab7b1e5ea40 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts @@ -38,10 +38,11 @@ describe('Swimlane Service', () => { connectorType: 'all', mappings, }; + const apiToken = 'token'; const headers = { 'Content-Type': 'application/json', - 'Private-Token': 'token', + 'Private-Token': apiToken, }; const incident = { @@ -62,7 +63,7 @@ describe('Swimlane Service', () => { // The trailing slash at the end of the url is intended. // All API calls need to have the trailing slash removed. config, - secrets: { apiToken: 'token' }, + secrets: { apiToken }, }, logger, configurationUtilities @@ -83,7 +84,7 @@ describe('Swimlane Service', () => { appId: '99999', mappings, }, - secrets: { apiToken: '121212' }, + secrets: { apiToken }, }, logger, configurationUtilities @@ -100,7 +101,7 @@ describe('Swimlane Service', () => { // @ts-ignore appId: null, }, - secrets: { apiToken: 'token' }, + secrets: { apiToken }, }, logger, configurationUtilities @@ -118,7 +119,7 @@ describe('Swimlane Service', () => { // @ts-ignore mappings: null, }, - secrets: { apiToken: 'token' }, + secrets: { apiToken }, }, logger, configurationUtilities @@ -205,14 +206,218 @@ describe('Swimlane Service', () => { }); await expect(service.createRecord({ incident })).rejects.toThrow( - `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Error: An error has occurred` + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Error: An error has occurred.` ); }); }); - // TODO: Implement - describe('updateRecord', () => {}); + describe('updateRecord', () => { + const data = { + id: '123', + name: 'title', + modifiedDate: '2021-06-01T17:29:51.092Z', + }; + const incidentId = '123'; + + test('it updates a record correctly', async () => { + requestMock.mockImplementation(() => ({ + data, + })); + + const res = await service.updateRecord({ + incident, + incidentId, + }); + + expect(res).toEqual({ + id: '123', + title: 'title', + pushedDate: '2021-06-01T17:29:51.092Z', + url: `${config.apiUrl.slice(0, -1)}/record/${config.appId}/123`, + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data, + })); - // TODO: Implement - describe('createComment', () => {}); + await service.updateRecord({ + incident, + incidentId, + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + logger, + headers, + data: { + applicationId: config.appId, + id: incidentId, + values: { + [mappings.ruleNameConfig.id]: 'Rule Name', + [mappings.alertSourceConfig.id]: 'Alert Source', + [mappings.caseNameConfig.id]: 'Case Name', + [mappings.caseIdConfig.id]: 'Case Id', + [mappings.commentsConfig.id]: 'Comments', + [mappings.severityConfig.id]: 'Severity', + [mappings.descriptionConfig.id]: 'Description', + [mappings.alertIdConfig.id]: 'Alert Id', + }, + }, + url: `${config.apiUrl.slice(0, -1)}/api/app/${config.appId}/record/${incidentId}`, + method: 'patch', + configurationUtilities, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + await expect(service.updateRecord({ incident, incidentId })).rejects.toThrow( + `[Action][Swimlane]: Unable to update record in application with id ${config.appId}. Error: An error has occurred.` + ); + }); + }); + + describe('createComment', () => { + const data = { + id: '123', + name: 'title', + modifiedDate: '2021-06-01T17:29:51.092Z', + }; + const incidentId = '123'; + const comment = { commentId: '456', comment: 'A comment' }; + const createdDate = '2021-06-01T17:29:51.092Z'; + + test('it updates a record correctly', async () => { + requestMock.mockImplementation(() => ({ + data, + })); + + const res = await service.createComment({ + comment, + incidentId, + createdDate, + }); + + expect(res).toEqual({ + commentId: '456', + pushedDate: '2021-06-01T17:29:51.092Z', + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data, + })); + + await service.createComment({ + comment, + incidentId, + createdDate, + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + logger, + headers, + data: { + createdDate, + fieldId: mappings.commentsConfig.id, + isRichText: true, + message: comment.comment, + }, + url: `${config.apiUrl.slice(0, -1)}/api/app/${config.appId}/record/${incidentId}/${ + mappings.commentsConfig.id + }/comment`, + method: 'post', + configurationUtilities, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + await expect(service.createComment({ comment, incidentId, createdDate })).rejects.toThrow( + `[Action][Swimlane]: Unable to create comment in application with id ${config.appId}. Error: An error has occurred.` + ); + }); + }); + + describe('error messages', () => { + const errorResponse = { ErrorCode: '1', Argument: 'Invalid field' }; + + test('it contains the response error', async () => { + requestMock.mockImplementation(() => { + const error = new Error('An error has occurred'); + // @ts-ignore + error.response = { data: errorResponse }; + throw error; + }); + + await expect( + service.createRecord({ + incident, + }) + ).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Error: An error has occurred. Reason: Invalid field (1)` + ); + }); + + test('it shows an empty string for reason if the ErrorCode is undefined', async () => { + requestMock.mockImplementation(() => { + const error = new Error('An error has occurred'); + // @ts-ignore + error.response = { data: { ErrorCode: '1' } }; + throw error; + }); + + await expect( + service.createRecord({ + incident, + }) + ).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Error: An error has occurred. Reason: ` + ); + }); + + test('it shows an empty string for reason if the Argument is undefined', async () => { + requestMock.mockImplementation(() => { + const error = new Error('An error has occurred'); + // @ts-ignore + error.response = { data: { Argument: 'Invalid field' } }; + throw error; + }); + + await expect( + service.createRecord({ + incident, + }) + ).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Error: An error has occurred. Reason: ` + ); + }); + + test('it shows an empty string for reason if data is undefined', async () => { + requestMock.mockImplementation(() => { + const error = new Error('An error has occurred'); + // @ts-ignore + error.response = {}; + throw error; + }); + + await expect( + service.createRecord({ + incident, + }) + ).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Error: An error has occurred. Reason: ` + ); + }); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index 943cceecad735..734cd76033172 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -101,7 +101,7 @@ export const createExternalService = ( i18n.NAME, `Unable to create record in application with id ${appId}. Error: ${ error.message - } Reason: ${createErrorMessage(error.response?.data)}` + }. Reason: ${createErrorMessage(error.response?.data)}` ) ); } @@ -136,7 +136,7 @@ export const createExternalService = ( i18n.NAME, `Unable to update record in application with id ${appId}. Error: ${ error.message - } Reason: ${createErrorMessage(error.response?.data)}` + }. Reason: ${createErrorMessage(error.response?.data)}` ) ); } @@ -182,7 +182,7 @@ export const createExternalService = ( i18n.NAME, `Unable to create comment in application with id ${appId}. Error: ${ error.message - } Reason: ${createErrorMessage(error.response?.data)}` + }. Reason: ${createErrorMessage(error.response?.data)}` ) ); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts index 4d896d74e481e..5233c6f23f6cd 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts @@ -20,8 +20,7 @@ import { ActionsConfigurationUtilities } from '../../actions_config'; export type SwimlanePublicConfigurationType = TypeOf; export type SwimlaneSecretConfigurationType = TypeOf; -export type MappingConfigType = TypeOf & - Record; +export type MappingConfigType = TypeOf; export type ExecutorParams = TypeOf; export type ExecutorSubActionPushParams = TypeOf; @@ -68,9 +67,8 @@ export interface FieldConfig { export interface SwimlaneRecordPayload { applicationId: string; + values: SwimlaneDataValues; id?: string; - values?: SwimlaneDataValues; - comments?: SwimlaneDataComments; } export interface ExternalService { From a8b9462e0514650a6a1dada1137262e0ef6db65a Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 15 Jun 2021 21:08:04 +0300 Subject: [PATCH 79/96] Improve tests --- .../builtin_action_types/swimlane/api.test.ts | 134 +++++++++++++----- .../builtin_action_types/swimlane/api.ts | 14 +- .../builtin_action_types/swimlane/mocks.ts | 38 +++-- .../swimlane/service.test.ts | 3 - .../builtin_action_types/swimlane/types.ts | 8 +- 5 files changed, 142 insertions(+), 55 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts index 03ddfff013faf..13c62b9ef1cf8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts @@ -7,20 +7,15 @@ import { api } from './api'; import { ExternalService } from './types'; -import { externalServiceMock, recordResponseCreate, recordResponseUpdate } from './mocks'; +import { + apiParams, + externalServiceMock, + recordResponseCreate, + recordResponseUpdate, +} from './mocks'; import { Logger } from '@kbn/logging'; let mockedLogger: jest.Mocked; -const params = { - ruleName: 'rule name', - alertId: '123456', - caseName: 'case name', - severity: 'critical', - alertSource: 'elastic', - caseId: '123456', - comments: 'some comments', - description: 'case desc', -}; describe('api', () => { let externalService: jest.Mocked; @@ -31,55 +26,118 @@ describe('api', () => { describe('pushToService', () => { test('it pushes a new record', async () => { + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; const res = await api.pushToService({ externalService, logger: mockedLogger, - params: { - incident: { - ...params, - externalId: null, - }, - comments: [], - }, + params, }); - expect(externalService.createComment).not.toHaveBeenCalled(); + + expect(externalService.createComment).toHaveBeenCalled(); expect(externalService.createRecord).toHaveBeenCalled(); expect(externalService.updateRecord).not.toHaveBeenCalled(); - expect(res).toEqual(recordResponseCreate); + + expect(res).toEqual({ + ...recordResponseCreate, + comments: [ + { + commentId: '123456', + pushedDate: '2021-06-01T17:29:51.092Z', + }, + { + commentId: '123456', + pushedDate: '2021-06-01T17:29:51.092Z', + }, + ], + }); }); - test('it pushes a new record with a comment', async () => { - await api.pushToService({ + test('it pushes a new record without comment', async () => { + const params = { + ...apiParams, + incident: { ...apiParams.incident, externalId: null }, + comments: [], + }; + const res = await api.pushToService({ externalService, logger: mockedLogger, - params: { - incident: { - ...params, - externalId: null, - }, - comments: [{ comment: 'some comments', commentId: '123' }], - }, + params, }); - expect(externalService.createComment).toHaveBeenCalled(); + + expect(externalService.createComment).not.toHaveBeenCalled(); + expect(externalService.createRecord).toHaveBeenCalled(); + expect(res).toEqual(recordResponseCreate); }); test('updates existing record', async () => { const res = await api.pushToService({ externalService, logger: mockedLogger, - params: { - incident: { - ...params, - externalId: '1234', - }, - comments: [{ comment: 'some comments', commentId: '123' }], - }, + params: apiParams, }); expect(externalService.createComment).toHaveBeenCalled(); expect(externalService.createRecord).not.toHaveBeenCalled(); expect(externalService.updateRecord).toHaveBeenCalled(); - expect(res).toEqual(recordResponseUpdate); + expect(res).toEqual({ + ...recordResponseUpdate, + comments: [ + { + commentId: '123456', + pushedDate: '2021-06-01T17:29:51.092Z', + }, + { + commentId: '123456', + pushedDate: '2021-06-01T17:29:51.092Z', + }, + ], + }); + }); + + test('it calls createRecord correctly', async () => { + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; + await api.pushToService({ externalService, params, logger: mockedLogger }); + + expect(externalService.createRecord).toHaveBeenCalledWith({ + incident: { + alertId: '123456', + alertSource: 'elastic', + caseId: '123456', + caseName: 'case name', + description: 'case desc', + ruleName: 'rule name', + severity: 'critical', + }, + }); + }); + + test('it calls createComment correctly', async () => { + const mockedToISOString = jest + .spyOn(Date.prototype, 'toISOString') + .mockReturnValue('2021-06-15T18:02:29.404Z'); + + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; + await api.pushToService({ externalService, params, logger: mockedLogger }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + createdDate: '2021-06-15T18:02:29.404Z', + incidentId: '123456', + comment: { + commentId: 'case-comment-1', + comment: 'A comment', + }, + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + createdDate: '2021-06-15T18:02:29.404Z', + incidentId: '123456', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment', + }, + }); + + mockedToISOString.mockRestore(); }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts index 5a28b938ecd65..343a94e52711f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts @@ -10,6 +10,7 @@ import { ExternalServiceApi, Incident, PushToServiceApiHandlerArgs, + PushToServiceResponse, } from './types'; const pushToServiceHandler = async ({ @@ -17,7 +18,7 @@ const pushToServiceHandler = async ({ params, }: PushToServiceApiHandlerArgs): Promise => { const { comments } = params; - let res: ExternalServiceIncidentResponse; + let res: PushToServiceResponse; const { externalId, ...rest } = params.incident; const incident: Incident = rest; @@ -33,12 +34,21 @@ const pushToServiceHandler = async ({ const createdDate = new Date().toISOString(); if (comments && Array.isArray(comments) && comments.length > 0) { + res.comments = []; for (const currentComment of comments) { - await externalService.createComment({ + const comment = await externalService.createComment({ incidentId: res.id, comment: currentComment, createdDate, }); + + res.comments = [ + ...(res.comments ?? []), + { + commentId: comment.commentId, + pushedDate: comment.pushedDate, + }, + ]; } } diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts index 654b39225ab8d..860dd38f4fca9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ExternalService } from './types'; +import { ExecutorSubActionPushParams, ExternalService, PushToServiceApiParams } from './types'; export const applicationFields = [ { @@ -75,16 +75,19 @@ export const recordResponseCreate = { id: '123456', title: 'neato', url: 'swimlane.com', + pushedDate: '2021-06-01T17:29:51.092Z', }; export const recordResponseUpdate = { id: '98765', title: 'not neato', url: 'laneswim.com', + pushedDate: '2021-06-01T17:29:51.092Z', }; export const commentResponse = { - id: '123456', + commentId: '123456', + pushedDate: '2021-06-01T17:29:51.092Z', }; const createMock = (): jest.Mocked => { @@ -99,17 +102,30 @@ const externalServiceMock = { create: createMock, }; -const executorParams = { - ruleName: 'rule-name', - alertSource: 'alert-source', - caseId: 'case-id', - caseName: 'case-name', - comments: 'comments', - severity: 'severity', - alertId: 'alert-id', +const executorParams: ExecutorSubActionPushParams = { + incident: { + ruleName: 'rule name', + alertId: '123456', + caseName: 'case name', + severity: 'critical', + alertSource: 'elastic', + caseId: '123456', + description: 'case desc', + externalId: 'incident-3', + }, + comments: [ + { + commentId: 'case-comment-1', + comment: 'A comment', + }, + { + commentId: 'case-comment-2', + comment: 'Another comment', + }, + ], }; -const apiParams = { +const apiParams: PushToServiceApiParams = { ...executorParams, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts index 3fab7b1e5ea40..bee13dd081b83 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts @@ -50,7 +50,6 @@ describe('Swimlane Service', () => { alertSource: 'Alert Source', caseId: 'Case Id', caseName: 'Case Name', - comments: 'Comments', severity: 'Severity', externalId: null, description: 'Description', @@ -188,7 +187,6 @@ describe('Swimlane Service', () => { [mappings.alertSourceConfig.id]: 'Alert Source', [mappings.caseNameConfig.id]: 'Case Name', [mappings.caseIdConfig.id]: 'Case Id', - [mappings.commentsConfig.id]: 'Comments', [mappings.severityConfig.id]: 'Severity', [mappings.descriptionConfig.id]: 'Description', [mappings.alertIdConfig.id]: 'Alert Id', @@ -259,7 +257,6 @@ describe('Swimlane Service', () => { [mappings.alertSourceConfig.id]: 'Alert Source', [mappings.caseNameConfig.id]: 'Case Name', [mappings.caseIdConfig.id]: 'Case Id', - [mappings.commentsConfig.id]: 'Comments', [mappings.severityConfig.id]: 'Severity', [mappings.descriptionConfig.id]: 'Description', [mappings.alertIdConfig.id]: 'Alert Id', diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts index 5233c6f23f6cd..56ddbda8e86af 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts @@ -55,7 +55,9 @@ export interface ExternalServiceIncidentResponse { pushedDate: string; } export interface ExternalServiceCommentResponse { + commentId: string; pushedDate: string; + externalCommentId?: string; } export interface FieldConfig { @@ -87,6 +89,10 @@ export interface GetApplicationHandlerArgs { externalService: ExternalService; } +export interface PushToServiceResponse extends ExternalServiceIncidentResponse { + comments?: ExternalServiceCommentResponse[]; +} + export interface ExternalServiceApi { pushToService: (args: PushToServiceApiHandlerArgs) => Promise; } @@ -103,7 +109,7 @@ export type SwimlaneDataComments = Record; export interface SimpleComment { comment: SwimlaneComment['message']; - commentId?: string; + commentId: string; } export interface CreateCommentParams { From cc5b10a4e9ffd89263e6d63b1e238c96fde7322d Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 15 Jun 2021 21:53:17 +0300 Subject: [PATCH 80/96] Improve messages --- .../cases/public/components/connectors/swimlane/translations.ts | 2 +- .../cases/public/components/connectors/swimlane/validator.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts index edbc19dde64a6..eb6cd168fab99 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts @@ -37,6 +37,6 @@ export const EMPTY_MAPPING_WARNING_DESC = i18n.translate( 'xpack.cases.connectors.swimlane.emptyMappingWarningDesc', { defaultMessage: - 'This connector cannot be selected because it is missing the required case field mappings. You edit this connector to add required field mappings or select a connector of type Cases.', + 'This connector cannot be selected because it is missing the required case field mappings. You can edit this connector to add required field mappings or select a connector of type Cases.', } ); diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts index 95d96df9daf1e..973e7d730b146 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts @@ -27,7 +27,7 @@ export const connectorValidator = ( } = connector; if (connectorType !== SwimlaneConnectorType.Cases || isAnyRequiredFieldNotSet(mappings)) { return { - message: 'Invalid connector', + message: '', }; } }; From aa79a78608b82cb4885f9d1cc8c617934ee2f148 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 16 Jun 2021 11:46:49 +0300 Subject: [PATCH 81/96] Fix bug when using connector of type all --- .../connectors/swimlane/case_fields.tsx | 17 +++-------------- .../components/connectors/swimlane/validator.ts | 8 +++++++- .../swimlane/swimlane_params.tsx | 6 +++--- .../swimlane/translations.ts | 2 +- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx index 2f15baeb2913f..947df7012e68d 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx @@ -9,27 +9,16 @@ import React, { useMemo } from 'react'; import { EuiCallOut } from '@elastic/eui'; import * as i18n from './translations'; -import { ConnectorTypes, SwimlaneFieldsType, SwimlaneConnectorType } from '../../../../common'; +import { ConnectorTypes, SwimlaneFieldsType } from '../../../../common'; import { ConnectorFieldsProps } from '../types'; import { ConnectorCard } from '../card'; -import { isAnyRequiredFieldNotSet } from './validator'; +import { connectorValidator } from './validator'; const SwimlaneComponent: React.FunctionComponent> = ({ connector, isEdit = true, }) => { - const { - config: { mappings, connectorType }, - } = connector; - const showMappingWarning = useMemo( - /** - * If the type of the connector is not cases - * or there any of the required fields is not set - * a warning message is being shown to the user - */ - () => connectorType !== SwimlaneConnectorType.Cases || isAnyRequiredFieldNotSet(mappings), - [mappings, connectorType] - ); + const showMappingWarning = useMemo(() => connectorValidator(connector) != null, [connector]); return ( <> diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts index 973e7d730b146..703110dc9946a 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts @@ -19,13 +19,19 @@ const casesRequiredFields = [ export const isAnyRequiredFieldNotSet = (mapping: Record | undefined) => !casesRequiredFields.some((field) => mapping != null && mapping[field] != null); +/** + * The user can use either a connector of type cases or all. + * If the connector is of type all we should check if all + * required field have been configured. + */ + export const connectorValidator = ( connector: CaseActionConnector ): ReturnType => { const { config: { mappings, connectorType }, } = connector; - if (connectorType !== SwimlaneConnectorType.Cases || isAnyRequiredFieldNotSet(mappings)) { + if (connectorType === SwimlaneConnectorType.Alerts || isAnyRequiredFieldNotSet(mappings)) { return { message: '', }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx index 50a6be9826843..796348e98f264 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx @@ -71,9 +71,9 @@ const SwimlaneParamsFields: React.FunctionComponent Date: Wed, 16 Jun 2021 11:51:35 +0300 Subject: [PATCH 82/96] Fix bug when creating connector --- .../swimlane/steps/swimlane_connection.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx index f489e7b916a95..d03be0b68ecd8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx @@ -94,9 +94,9 @@ const SwimlaneConnectionComponent: React.FunctionComponent = ({ } }, [apiToken, editActionSecrets]); - const isApiUrlInvalid = errors.apiUrl.length > 0 && apiToken !== undefined; - const isAppIdInvalid = errors.appId.length > 0 && apiToken !== undefined; - const isApiTokenInvalid = errors.apiToken.length > 0 && apiToken !== undefined; + const isApiUrlInvalid = errors.apiUrl?.length > 0 && apiToken !== undefined; + const isAppIdInvalid = errors.appId?.length > 0 && apiToken !== undefined; + const isApiTokenInvalid = errors.apiToken?.length > 0 && apiToken !== undefined; return ( <> From 18c37d73abe5015a48a9e7796038ec5e76e1c074 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 16 Jun 2021 16:21:15 +0300 Subject: [PATCH 83/96] Add cases_fields tests --- .../public/components/connectors/mock.ts | 18 ++++++ .../connectors/swimlane/case_fields.test.tsx | 53 ++++++++++++++++ .../connectors/swimlane/case_fields.tsx | 7 ++- .../connectors/swimlane/validator.test.ts | 60 +++++++++++++++++++ 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts diff --git a/x-pack/plugins/cases/public/components/connectors/mock.ts b/x-pack/plugins/cases/public/components/connectors/mock.ts index f5429fa2396aa..663b397e6f4fe 100644 --- a/x-pack/plugins/cases/public/components/connectors/mock.ts +++ b/x-pack/plugins/cases/public/components/connectors/mock.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { SwimlaneConnectorType } from '../../../common'; + export const connector = { id: '123', name: 'My connector', @@ -13,6 +15,22 @@ export const connector = { isPreconfigured: false, }; +export const swimlaneConnector = { + id: '123', + name: 'My connector', + actionTypeId: '.swimlane', + config: { + connectorType: SwimlaneConnectorType.Cases, + mappings: { + caseIdConfig: {}, + caseNameConfig: {}, + descriptionConfig: {}, + commentsConfig: {}, + }, + }, + isPreconfigured: false, +}; + export const issues = [ { id: 'personId', title: 'Person Task', key: 'personKey' }, { id: 'womanId', title: 'Woman Task', key: 'womanKey' }, diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx new file mode 100644 index 0000000000000..1a035d92611bd --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx @@ -0,0 +1,53 @@ +/* + * 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 React from 'react'; +import { render, screen } from '@testing-library/react'; + +import { SwimlaneConnectorType } from '../../../../common'; +import Fields from './case_fields'; +import * as i18n from './translations'; +import { swimlaneConnector as connector } from '../mock'; + +const fields = { + caseId: '123', +}; + +const onChange = jest.fn(); + +describe('Swimlane Cases Fields', () => { + test('it does not shows the mapping error callout', () => { + render(); + expect(screen.queryByText(i18n.EMPTY_MAPPING_WARNING_TITLE)).toBeFalsy(); + }); + + test('it shows the mapping error callout when mapping is invalid', () => { + const invalidConnector = { + ...connector, + config: { + ...connector.config, + mappings: {}, + }, + }; + + render(); + expect(screen.queryByText(i18n.EMPTY_MAPPING_WARNING_TITLE)).toBeTruthy(); + }); + + test('it shows the mapping error callout when the connector is of type alerts', () => { + const invalidConnector = { + ...connector, + config: { + ...connector.config, + connectorType: SwimlaneConnectorType.Alerts, + }, + }; + + render(); + expect(screen.queryByText(i18n.EMPTY_MAPPING_WARNING_TITLE)).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx index 947df7012e68d..b6370504edbb6 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx @@ -31,7 +31,12 @@ const SwimlaneComponent: React.FunctionComponent )} {showMappingWarning && ( - + {i18n.EMPTY_MAPPING_WARNING_DESC} )} diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts new file mode 100644 index 0000000000000..c565c766c0e77 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SwimlaneConnectorType } from '../../../../common'; +import { swimlaneConnector as connector } from '../mock'; +import { isAnyRequiredFieldNotSet, connectorValidator } from './validator'; + +describe('Swimlane validator', () => { + describe('isAnyRequiredFieldNotSet', () => { + test('it returns true if a required field is not set', () => { + expect(isAnyRequiredFieldNotSet({ notRequired: 'test' })).toBeTruthy(); + }); + + test('it returns false if all required fields are set', () => { + expect(isAnyRequiredFieldNotSet(connector.config.mappings)).toBeFalsy(); + }); + }); + + describe('connectorValidator', () => { + test('it returns an error message if the mapping is not correct', () => { + const invalidConnector = { + ...connector, + config: { + ...connector.config, + mappings: {}, + }, + }; + expect(connectorValidator(invalidConnector)).toEqual({ message: '' }); + }); + + test('it returns an error message if the connector is of type alerts', () => { + const invalidConnector = { + ...connector, + config: { + ...connector.config, + connectorType: SwimlaneConnectorType.Alerts, + }, + }; + expect(connectorValidator(invalidConnector)).toEqual({ message: '' }); + }); + + test.each([SwimlaneConnectorType.Cases, SwimlaneConnectorType.All])( + 'it does not return an error message if the connector is of type %s', + (connectorType) => { + const invalidConnector = { + ...connector, + config: { + ...connector.config, + connectorType, + }, + }; + expect(connectorValidator(invalidConnector)).toBe(undefined); + } + ); + }); +}); From 57ac290b28f934f636e3bb5ec6b565b6eb6cd219 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 16 Jun 2021 17:11:25 +0300 Subject: [PATCH 84/96] Improve docs --- docs/developer/plugin-list.asciidoc | 2 +- .../connectors/action-types/swimlane.asciidoc | 27 ++++++++++++------ .../images/swimlane-params-test.png | Bin 168149 -> 175258 bytes 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 6675a8dd283c9..874aa19828ff8 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -321,7 +321,7 @@ which will load the visualization's editor. |{kib-repo}blob/{branch}/x-pack/plugins/actions/README.md[actions] -|The Kibana actions plugin provides a framework to create executable actions. You can: +|****# Kibana Actions |{kib-repo}blob/{branch}/x-pack/plugins/alerting/README.md[alerting] diff --git a/docs/management/connectors/action-types/swimlane.asciidoc b/docs/management/connectors/action-types/swimlane.asciidoc index 1f29d749664f7..ed9a21605b4ea 100644 --- a/docs/management/connectors/action-types/swimlane.asciidoc +++ b/docs/management/connectors/action-types/swimlane.asciidoc @@ -5,7 +5,7 @@ Swimlane ++++ -The Swimlane connector uses the https://swimlane.com/knowledge-center/docs/developer-guide/rest-api/[REST API] to create Swimlane records. +The Swimlane connector uses the https://swimlane.com/knowledge-center/docs/developer-guide/rest-api/[Swimlane REST API] to create Swimlane records. [float] [[swimlane-connector-configuration]] @@ -31,11 +31,11 @@ API token:: Swimlane API authentication token for HTTP Basic authentication. apiUrl: https://elastic.swimlaneurl.us appId: app-id mappings: - alertNameConfig: + alertIdConfig: fieldType: text - id: a6fst - key: alert-name - name: Alert Name + id: agp4s + key: alert-id + name: Alert ID alertSourceConfig: fieldType: text id: adnls @@ -56,6 +56,16 @@ API token:: Swimlane API authentication token for HTTP Basic authentication. id: au18d key: comments name: Comments + descriptionConfig: + fieldType: text + id: ae1gd + key: description + name: Description + ruleNameConfig: + fieldType: text + id: avfsl + key: rule-name + name: Rule Name severityConfig: fieldType: text id: a71ik @@ -94,9 +104,8 @@ image::management/connectors/images/swimlane-params-test.png[Swimlane params tes Swimlane actions have the following configuration properties. -Alert Name:: The alert name of the incident. Alert Source:: The alert source of the incident. -Case ID:: The Case ID of the incident. -Case Name:: The Case name of the incident. -Comments:: The comments of the incident. +Comments:: Additional information for the client, such as how to troubleshoot the issue. Severity:: The severity of the incident. + +NOTE: Alert ID and Rule Name are filled automatically. Specifically, Alert ID is set to `{{alert.id}}` and Rule Name to `{{rule.name}}`. \ No newline at end of file diff --git a/docs/management/connectors/images/swimlane-params-test.png b/docs/management/connectors/images/swimlane-params-test.png index 2dca1850964b3014ab90b2412b391d767b18939c..c0e02c2c7b18f2fa4118e10d31d0a7c6447d4c19 100644 GIT binary patch literal 175258 zcma&O1z1$=w+BiybO?w;4PDYA-O@@U4IbW5p#O_uW z_b>K3e^41(Leb}iiad&ByJXcSUNwX|luCY+fB_|+R`oT3N&Zl?vsU3NWR)GcU*T8= z-iW5gq?w8(8SRP{%2bI_B>~>MoiE1-@31tLBy1ousm(nKAF2;464qImk))LD0?WlE z23Hed3U68(((3N*)b7MzcE2v5*C`O6$o{+A1?{u?X9=pVHJJwi>60c(CKZ1CR`BaQ zIWSi7SAl}1!tY)3$XUYs*has9)0v{xsQgwLCxZGy_C=h6c2Ax>ITURN5kE5NYBrLl zQ!KLecMZ!BorhQ!O(^V(B0T#9nv@uj8p-iTpO>$M49e8FxwLUn3VXhks?puOD6)qX zk&VN;$Y4vaJ%q_M%U^Z8LiP`$jiilaKzY-}63GtX>9WQCq4YW(*?;$|5J4`lc2pDP zsFedm{ew*1|Y6Mf&4w$~`4-|=w|2A)%i(d_spE!rR+2ipB&MEPBiAtV4Z zHM#k#<`AAVt-D5V0Dd_GE;dD5($l)$_N7eJYJ2Xkjh&EXwXhZXl-B#mRB#COMr&K{ zX@rr~@d|dV%d-E}Y$vAy|0Oae+9U*y*@v(amHCA}th||u$Mf6FT_jjd2)vpg6rpm$ zFDzPEzK_oDp!^qQC14-~;OD%9^ic3}4+awX_1vF&eda&;U~w(duJU5S+b&sMHPal) zmFpkEnZ9}W-0vTTc+%(pZs_6Q%y-%fj5qow|JCuY&u{yLq(9QI_~8qol20!MI796R zinp3!nio}b?6Yx3Se^T2`@L=yj-lUT0j{aRrT@(JRCSK=?Hhk)+z-M3<0t(`*pGbG}slV;AS zF7l(-H`g?FefW7CZaEtrm%9gf1A!MgBn%~OlhC`b98q~f*mcjmgNWL|KT#%i;V}w} zsA2QXnAL9u`MCxoHc5`E^S%a4b$>Ve&XXiSe{&2!a*Rt5!KcQmMc0kJ&Zc&_2l-(8 zn>QQlt%{M#b10kx@Y>1_gWe+1C+2>S5e9F!F)uYQFRu{}tx;5{^NZ#^ucO5NIE0j; zGAPcnFzR?b8i(p-VQ*Qachw`P-bg>8Dy{j8nx9?zKroTXgkDO(<$fS5D8($Yqy36( z_{k*|^u-(Z0|O0WV)Of-&vv12g!qhMI+|&%i8T=>erZ*^ayLs4qRspA!wE@%(9*~e zKGS~m=Y$k}=D_y$BO8+7jSkKrSNn_~Ma~ulFM(8wOe}y*ih|*RIK5W{Z0;P zFU{R)wZjj7?;Jk6RK6vh5A&iN1c|{Req!|zDc53xC3Yn1B*i6FB#1J! z*&Wyw*;Oi-D_*geOuJVoRA`xsj~3B=MFBgBKQ?3-X2FdFucbS?BQlps{LH8ij!04ads#e1IJ?w=Uft!KWc@YikEZfC_iUaTL$ ze&AL7f>${l)mEPgrU}AXzMEDL9cJiGSkEnawz{;21Rjuhc7WJRIf6M7bj!y!Kgmaw z1ld%V{B~|QvRPq#79bfZS=j}SA*V{pvLAs`jM@T^q-QaGb$yG+7O&RM`)l_#8*2!2 zOriyg8T$@f6^kruidEypZ6kbJ%M97b$1u?l(W=bcHRGOdaqMwU1)cBb-wWqEyw5J= zSDjJKR%I$Js`9iCmxDn|ID4-aa_6_kBzmGu$W_95`Hu6; z0_}=UUT@Sfax+FTo@jx!=Buacjy35ui))5vTV~g**{d5Iovfc%TiG|Q30>gtm2{@J zt%PL9XOEVRmyPrc)6eA(?_^aKIeouSIAG(4OZkw(#s7|<#DmY{#N*=Ter;njex<7C z;EEaOdm>9Xt2EfFuVwIq^*u;j7m$t`U| zxp-yRvw4Q;ZQrj3(U>X;E4hrO8{Zh4*smDd+plhpr607Zwtd#y))RV3?dY(3$C+47 zYts}#8XiQT$g*xyFMZp4If8WRCy7*tJx`55%_v)+sGmq7YbrVZmN-Q->6Vm|!a!N- zW-?08D2y+~^edf|g#>vfWwwRv&Wq>VmV4FVN5m^3?F(N&u<1_wl|4hFUYFtJm9)cl z8uqalc9ps=Ivto*wZyHW_KlJMHq@tgGJd3e?0ac;nFQnYi|$w1+lOA|Zv$nwW$%p~ zkYW8ZTNtr4lo(x;h@u*3Z$xCw`@_HA&wiP0a4~h+atZv+y-au%)I}AO{mwFmQzDpR zPtiTQAZeH(EZQ=jv=pf{z+LN7-b=`m{z3BoCnPoXN2-(Q@hI#_d3I2?)>lgbkFHZA ze_j8k-e#ji!}8u&iCk9et!f2o8iwR$Y=%|6C11;|WjbaLYj`%~*VlUMI2T;*^R5o? zGF}>M8rNIzw{!^jKPV|?$Wt)X)^u78Wwl*?99kZyw3Hrax>E0D;#XPLR8T*5p|#qo zNzw9Tys7b0TQ417OYY$)f)rsESC_C_uk|ZW=Oe4|Y4f={%z@Vvme~)+Wz^j&M;q)% zxVyQpYR^kIO9Rg0c2o);G#OtuX=7=YR1N3YO0ET>-J+?yMzAQD)349fAIa|f(N|UJ zXO){@T0}P|HLL#fY^!qBBHQceKGU^s={$N*$MLgM`9bJ{%a7^&TG#lYqBVBou=*^! zi_5wA3%^~H9EXCr()f}peUeG)g^T5hQyr__i0K8}<{QC1tM^UH#2F+ni$u1wCLAVE zc4i0^2&U0RiA_yT&3G^CMhA+fhrM!iB5u7%cBm3%huGx39jp7bXtGX!rmGQnKju}9r_?nX3ZqAZE@pw3G z%#+X3Hx@fIo%#&<<_YZHPtNR5d0y&vwxUE}6VG|{y4@ZJ?RIU1`^%chN~e6|&-dKF zuFqvFI5`fnVdB=7(mr>0x&0*uit*#K336 zhcUm=aBw3xYc^PSl77)KlA^%Rx087N?X+e$WG=qFSYGGgY~$=gx>{ksIU5RrAmJNQ zb0QJcpaOJis~v&C_v4qGkk7}r+=7b&CCBxgkYuRQmc(VYg%kmUJmhs9@R4 z-w^NG8Z`FUPS!ci*v6bgiZ`2k2VQ2VP`Oy%OvCs0jc9jwI+efltzZXvMO_e-L0HKz zUI)@_o^OAVI`pL($X@6>>fn*nUqtt;e?URcjhLI((9{C_k6;5;NyB&Vpy+^W5EL9V z7z!S^f(G9F&;He?4Mv#z&_5P7WesWorO=&B)BiOvaB4fk1d|^$ocd-iZCDIq(-B8335p z+)PYPPEL$YY>Za6MocVRTwF}dtW2z|48Ri%b}p9oy3P!icI5x=nO)!vv`0DA_E!Oz0M!O8ot2LGR1|2yRWX{!2vnsPET|KFzn=hpwbsj{7ct%#Kc zFsMEM{}Sv!jsNe>|1{)f`m^@`vlsu~=zskR>@+_zFVp`}jUSoE#Q|`Pp$JXh$SMKf z0Gs`J!MXq+H2?kvuAyahzLS{ELO}^aNxl(Ma)#cWbDEVoBk4FA@K|{5bgpePV<0B1 z?-w8;rf*Pe9xO+p@d8&k`dbnaE7?xA+?R$6s>GYdyZ(z!!j0M@JFN@ti97Cy!2YZ7 zvgD0nl7<`4CjPo}Vz-y(Ac!{<%o87rV=$7~)HpgiI!C0C)9r)a9@3{9q@qwrC#Wvm z5c0dLk_cU19*y|;{rdIGH9b!{ngU7xiJ|`0FD44SySqDKayLzW!o>ARep|zZB3?$lx zfT7cBtFf@LF>BMah@YAe(973n*60XhF{-da8>L_&d>kAK{&+^M;nL=8#gM#kG0vRx zt{7?+<-bw|Qv}9QH}y>Z`%Iw$`Kwo1;IkUrwQfDV2BErCX3=1raWlKnC&>!zb;+A% zFdR5yRnrK;cRIlj37Y~lr6-;#(F`*$=DUb6jIeJs-&33cbiqQsNO;R75TsQnpM6e8 zs054XQKa3FXYffjjllcgXh|Cw-z=XQH&ZV}0~T-Y+m-mQ-xFD}Tpd zXA%Mz3N?5Y^?j$W0#vM3XILa;?B_e)%Nr6K^2#!Dog)t9; z5DVR4gy~8Cwvyl9y|K>yPvQ2jb0g(2K!rZ3XR7dlCL>vBTi4~}Pm8d(;7d&F=csw& ziX$O;w*wp3XFKa)KY%;Iq4NG)!^kQUCAFWu}2f4kl z4}+XCDf|sKjBZZ|?!UzPGc1f4jj@MP?8B4{izyLbPB@!>=LfzBy}vf}hjC0xJ!7#D z2bjp0PLu*tUTX6fx(S}5_~G39uLp&iUC!3KDE2^;NXyAZ<}_UEy=BWgf5B#m+IP0; z`&Ws2+YEvq)5|543nLqa6*l8EhVv7XkPyW0X(WsIOKWdLXfqn)y{uya544t@iDI*F zi!o8YVw}cv&oI@e{w4zuM^H5i%p70Rb}<6_^=!3GD<$NW7)G8)*4uBhTE4D--86C- z)5z3mLKtCO5O^t-B{FE|eoMemG znA2gzN)7-Dhm#lo)+751GkPa^BO{KX{xG!)huEWo!iN0d{2cv8|DuL4dEi5%KK09P z{6t8z<1}V?hRHo5{}&{IT*J@8d+B;$rzCP%@mC^zqT_niZJI(U9rsf^=KinH(F13m zjnCd9RKbyjPYDS%& zBC7*H-7-uC=x&^3?hCq z%3$2spl9vBK@l%7!9EaZ-B5Q#%it%a2(t%Xsd=JEWWw6tZ`_*MDjDhXBgX17+2(3>vS~1^$YBm$J=@68HqILT8hV&}l5LgCJxi(3QtuFndwEnUO-NVa92f zDiEC{s@?@gfBBMS`5>j`PV`4jTL<^2K`sZzA3@{6%d_HG|RfqI^wx+*geYP?5&U zYC<7OzfzAjj#fI}vgCc4e42QeKe4RQ5vjvT(l1OW(##4_kd)6iR#r|Z05nAb`pxt4 z9$ep1@PCqry}d=783|7>WYaF>EJ|1?l(xlmGnQVZe5K#BLMS#@uXowU*mv`K^jjcU-S&WukH0hL8rNhEZ zfDk@y@w~|w{Hz!Xco}Fc6cE%Ir<(LIyxp>+G{T1a$)z_i!iea|PlNi<%hXZe_9!{1gOm`ZSW{sV+`lpp6oq%M=h@y96oi`Y4HEQJsU)6Db>Lh5cq+Vk2l{wyeRbdb=*Ol zPLdQf&K7>!a2B*j%DElr=-Al8Ne6Z?JLSha+S<$14O-EpVllzZm%=Ze-UM!VCBdB6 z9&l+T`>7`&VO%i(&2E{i_&w>5ae`a-9`dUl1?#U|AW-YucBH4tDGyn+yE-;tcqvLGyId?5TZo(Z0UzAA=tBLY@r4mb0s!>$fhCo+k~ zSuFfCCJ{k=Qhu4IEt=18(0mk1&}mr6fahEQ2C$e?rARZ%I=|Wli9K_Z!lL1qPI#aN zK$y~So66RBMVScElQ;k=LI$WfF`;tG4hoa^ zHMQakFNR*4m;U9->8wE@gs-Ndi2-`JGHNQ&Xksbl?A`f7clN1X=~}dm@CBkku{L=i z21!N3=uWY646h3e;WQqj=F6nIGwU2S^C}C{+rqTF*2ozS_709hbB!Rt=6kms&^Wl*I@)n`DdkCixWTP z6{pItIA-o~yPICx8NREsNXLGP_dc7!SZ3pg6+@>bDA><@Tco)Phx0oxC?SQo?}a6l zIxXv?++JBBZwq;jDvl&9oOU~-@>Mhy0Bf3 z()TR#(3A=GLAD|mnk-YX`o`vzTxi-*>!9BoPp&mRx)5Q`MU+if;W%At;>&wuE@Ot< z0s1Wo?YU!fTy`}0h@5yAdS6>Lnx@9|{1MekZLB==)B2S9i|=NK95)3E7iw0TlgM0b)f!A|EC$7mmjy+Cg}`4Hx*c__ zjs3HhhiD^jk}j?F#FG#x2IuiX>#pA6DNx{SoqrT|p-n7q6+J+J@2+mWS#56OAlwf7 z1eR1~(0iSZPk%m7h)K5y7(iW{EGzm83lE#~1GscFT6K=myHmSOuxXM7muknDLy%@G zK5%3W{3A*TGYkeD5Jn(WB-a1DGpSqry6L+dE5(IP^@>*{yi(5Oc`FnICjKm)y|1Nwltr!xB90*^j*j-5`eng^hArrS}Ps&Mn|EfGO!L;0NeuGkL znIeskvisTv86=Iv#Ml$}GPKk!@T>*SPZu{`0A?Q^1e%)}{Pc(!^jjI44`NTuo^$W} z^E!RoZmkY!lF`YWu(6PO}M5=M$DqZb*1`!Rxi`q!0r=Sde9^;2p!KGEf zPyoGjPV^9y#Pjg8g8oFogjTK8Z{G1}S@au+SCnVLy(cOOSNA!g^3+=b`EOiyhGlm( z8^~hU5?!!}=-umm8mqHccDC22YFu9T^%T-^Z|kh6x*|z^?`MmZXqbS2BYO&Y0phk7 z;bg5t-R}HJwkHWY?#>v!3Z+GDc{IBjURN|%2oBSGrS3QnNp}yT>|qBdCfRv3g3);U z>58$e#;b1S{Iv)H$F45L?9&^=fha9x+?O=)DC_a@QcX1X_xJmj=@SC3rxK%995ghO znL2`kmM$x2+lF^T=^gr8rE9j!za{THk)BbEdTCspY{dK!MV2PDn4UVcblRVd9Wxsb zuI+eX(bqGQJT^UTRWbXsA3r!yl4M=Q+7OcP{qlVjOl}cc!%lp3!;&a!2oExiJ1P zuRoBi{9qAVd{~MzXf9mGpjoYYlT>@}Ha;;Su3llz81#%f4iSU6^Y#kGV5gGEcK6ce zXvDK>s?;dP)5EPUNrAKcBAetf5mA8yfgWxYu+Ttkriwx5^zm}G*^WsH_T440KeF(` zI?o-)jYRK)av8FKScUW3yL-f|?VTgrwQc39Vx0mBtZ%1#&V1HLW`dj_7B^V0>T)Y5 z_9q*Y+-EmL8s5P(oe{h)-FvQJu&2(xwo}~hH_Gta5%+B#=FW}Dh{49ed0N(qD7a}t zj{o{+c7|lkbWN9q8oWB2QC}R1=T};4*%aHE0preZe1Wgb##O-_uCme8vW7}C`+P1; zYUP3$ub(qsOY#%Enq1)+_)Z%WN^s(jClgoF5Rq5^!~dJ8^p{c#6``r`Y9njgV`jrU zt^B2~d+IZuVg@noVHsWv{G^R3LppoPKdGGLsccAraZUHZ6apAy5dPv3+p5J0;M)$K#~Azo#jm zw~8`+dzYnfQV3pNS-Ct=*=UTVQ;8Q5N9R%5S<~XF>N+Ff$h3*MSi6Ovz_b|)&Q!=a zJgMAJ2zo@ookP6AggFS2mzuTqTO}+FIV`4fdt)hr7JkzEvo#HCNqmjsZ(v$lUbtsA z;h}TO($?-==mF;OYbD_{C?a$zMfcNQBdvPllSFHMi4#SMvfbBiXh<3hqpSp!xf!H9s)lj>| z;77M%uh^^O)!2hE#E5v`VC3PZ}EWxirD`8Ur=cm4s0T%0C66uhw@;q6Jd) zATN}mb>F--t-lJOiN~Rhp5e}sl7(I>O~I2oiL;mu=!f&OR5N)bCc%UQf!rRX2XH{# zc~Vl!8TY}Hxz^UROCC}8F_JONQK$6BKQe;WBbjS~*p3{B*}T+v;Q}U=2<~D|E{@ag ztclzFCX<-NovQ=UG%x)7@k81?;b(z?Hi~kJ{I88CUumW#okfo{J;>xK&{_nX6^#l1w+6kSL8eaKV|Gm)*paTx4Ofb1Q`Cv6@}cf92fq;3zd>$Ut`5FlyXlApKl2 ze0Qp^N4(9-<9Icf^~2!CI$w)jq(;3H1^@2^^382lC=~Rv{51zpY^T==ivx07( zfm6EwVQ6MYfSE@cV5(0_4%!p&D24l_K;B_t@}922B5*1@yfXpC$b6683qIQ>r0JN<_u}ST#sL$M29oo2AGqcCG^_>wyGBf zfyoG&<@BwFu{O(ZeuDABN+Rg&mU7!QdNCSvR?8yc7KTKQgdHUCndt#7r2HJErvy2G z!%~$TNFdpA_9+T(4zh&$m*|8?*E%ee@JWYx7bq6{SZJuy8{IOG?M=s;nvQlCzjxvx zV|;m`@M~szjGjU=n&R3diU`wkrc!IW@kU#fxXWU})}Y6f+&_B;p*$Hzxa#?Bv5z-1 z1?luZ`}xfpa7z1vf|m;us6C*TQQLRuyW07b1&zby3N~9xdtC7ge(HACoA-RSOf-X; z3@>qisng&)@5E6IigB;eJp|9bi&L7uq6|}pPE$wO50P(eJyv&JfVn!6Y!7{*)nDhh zSNnxlg;e1a-A0Hw1?2YbLQ7$XsADLwf5J({^3u}d1-tFnI>U%F(}v3X6!gX%`T zJEVCJdZ`Baj-*Ac+572Y-(A!(hMnWKmGiru6sc+p2%h5t`LT`9x*{1vZsET)mEis8 zpynPfQW#0c81@cKGwH@GCaT#qM;i}M212}oagHbE8;HF&w)tl&%}cceN|krcOHI#A zcMQnw3$ipkikIrH*~w0-JWtl&<;oq6$=w+W-^_pyB?V08^89;lfHYjBoX;b;#R+2m zH0UEjQ4X!UeIvfsd@oR5Mv|yP6U!9;)>wZ2B>|!e@$5wQ3uRqh)v8pti_;Ixm%m^o z<4Myq=&pY7zP;3^8=1tz3^CHI+G_UGk229KA*W(O1nDV zh_wT(MJ7OfZkDX<)^d;j$wO#M+`)Ipu{Eo&S=SqkAVlmOa@n8DReRiouQDl3ieN-_ zw|U`n=3N}N@@?CEB<9tXvl~dV44FCvyTm-Ik_Kp#;rExHZs{|1fjM*5AqstL!2hxBtce=qt*(rQOYe}}(y(QVWap|`;$K6#2O*FLFbuxEcYm?Ad zd)@HTRyQJhA`(CGQ2DPfbbtUx9>cwL!J-}y@&}EqcISkF+-AS_or8|_0=28ny0VVb(Wx_Bh)PaPy`3JTFD>a$?M!{{X*bK1DNn~eo6XT z(sV`Wk46kyAmn;tY~eb|q3v|Xz@zyY<`HXeu?>dqp)Bd+CTNj2%PWZ)u_K!eE~beM=`N&cX>8pbiMu9&(^SfxX{&U9 zB+%J!JMH^|WfisZjBf2NWb7<>97JU2^E}#y+Y|~^Cb%kj-C7bkiRDH9bevSJ&#|hJWhnyR1;Ywy#@Zh?O%SNhpGQcgl%a$-;bGCDA>B~ zHk{)6!a{J*!)d*ks+4ng?KWIHs=^3^ZBJA(%|pXI6ACU0z1P zUcZVJGmwBz(}Ea&&p$72kXJn4pD_Y9*v;{PZm!nxb(Wv?6qV?UU-@@VErCF zYmEIRGFL1MU`QQ>a-LGqqqA~R(j!Oew^0Eb`e+=CK2%&J8-ztMSEcOo@$6NLV93=uyw+fXRJ1BKt z?l|66SL0x6H`)asWAJb!XWMH-KAbV;)>!Uw1t67#uqK&YG z?q;&qL(J(`^8EmQGE9r?%|-0%!nwencD|{{G8;!i%?80hAUSGvy1AK$+A}63zCYNi zW%b%vsOinU?mb^lu2`U{Vz}XO7O}%T!rrCwc@mew`+Qo_ljlsdPGtt{JF`T;+kt@g z1KxO{!ww$f&h#ILm)mJ)>0BXOmJmy)uY2TulaOAOSw~x@xAnt4?6K(?$86*FL**Tn zTA88E4Dob;i9l~;{ z;M|LFAhp?*A-N*PYTgNn7yu7n+#8%p(W?IRc0mKERGu4?xt*`v+xTc2<49icjzcG& zR%|R%m>n#^-abJqHgY}xW#!?P3d6S|_YK1iO2CEqk1WEwJYAs~2c!++YYn`I2Ju8} zmXcLQt#(HsLVD9)>HYJ~(Q>JImvEgpn%&%Lm!d&)>VU-nipAwnrWuCXPveq3_GJaZ zYQQKl8O#aVbIa+D=|DFzo2In>vsY2+Vm)7N&ePA84(p7UW-*!0WRjc=I{K3;?`j?Q zDH>cI*=B%b&HlLiD;acJW@Ix#=fwkoHl4)bi8;JD@54fM2mOitxqU|5v}?Qrju@d7 zJ|r9R6*vDiT{56Tfwd$cc|TeF4uhV{Zo_Wkr9giI=~|BfS||V(Y@T1~V(FFb-59|C z?4LmSTtZTh1=ulrbm4T}F+hRE{AAR!L@^%(yh=25nCk^XuPNCiH@dmZWp!NF>USlQ zNB3NP9jRUH1UWt0Uv7<5HO-mrD83?|ZJMa;Q=iIna8s*rt*Gec1(0s3a*rp&@-AS# zjE7`*p-Md2W&Op*HgkMO=$(|9C(HDC{YTN@&fqnHsY*5ag@U8X-K!lYvsjned~uGg z>C70l^KtJ%MG4bJcTS814l}wG3~*}7-jBr-HGYYA4Sm~RkI?Pt#UtDs_)no;Y6|5ht%BaSbb4h)HnfK`$Ty(`sMcb7TC*Wwnch5C`#CM0Y zhdx_y9}^SIVdK5(*h3T=HX0(G^0{e>N?bDwd#n3c+jQyjo4l%oEIH=qOE}o zf@(54+r1{8VI0+BIk!JoN9}Sfuu;j!2=7;AITsBxfa`F>sF>|ePseFn#piq|Sb1=n zVqph((?Fue=&TY5S4qVfGMwtwdF{@f1Onm>wkGfZ>e3kfLmEEdXr1>f^0)%AuK(I$ z7b>{E2%dBd2^?h>C}sw4nkF)(Ho$}&kuCG> zuJt#&2w#JbgM9L&y3QPcq7ebJjOOUVzF=|xtWis{(H~FTz;8Q491kOWhMg?7u3R7| zWZ3t8{alL=UJ}U3;2NY=`Uav)uc_qMHS~Mm-(@(MT|h6d(oWY#rA!qQbPT9aiTx7Ws7K*4l<6ckV4s z%50!OW_*vsf_sQ^y8H*$Ts;>=NpTL(E3I6!#&wKgea?vg#N2!>WsS%4}mBW!(RKDKE>wY$~(kT_co~+yPNYX z3GQ+$v&vWo6?vIteuKC|M6R-p$S{I&i63u*3dXLF(#0cIV}q6XlIt9L!~#!RRxC6Z z|5t-}?H+nce!n!K@mlLvrd9v(MOq-e#RV=w-fGv)YgWe)Fe-6~&iQH`qV}gr^muHR zGrr`@bX~rWir$n_px-A zUK@0rGj|>6l5sD;*_haQZS-roh$dE^-}<8%CY^nll10gU6SO=|=drzI&IL=jT>vlJ zbO!V&(KT#PI7bw|Gge5h*GRY}XM`hsY(Ab*g8Jh2+mO}*P_AVy_n0JV!YMT_0aY3L z!SZ@@LdqtD`Ke-eT}eDuJzYnc%ZHhDfamvSCeU$5lQMABi!?^WiCx`V?NDUuMfe#4 z5#y;2X(hZLJPJO*WVBF-Z8o zIPURuZC9A}dG;fcoNd!4@iIb{^MyuRvoVoXo;5?(O6l88 zWY)ODo%Vw9!Vb!b`v$B-@j+7}1>pFyC+;saG0nNT1NjUFww>=7=La+Ch%xN_oi@5)eh7KYmM|pVR2w~^~3xCc*^3}6*&%dMek^@MGif<^7qKiLM(s~ z>U{oE-I>8?r!IOtkW@(=H5=?q>o1Z!8>5S6FneEPvl3u_ZxPZVAJ~2s^#Zd|eGkoo zp$fJ=Q|r7f7$=HM6s2dXY(szabK0M%Ks-OGa!yksF1?B!k9sesPS|f&;TBx__g#Q? z3!{o}Kvs3ypIl}pI~?5_QOKI5XY?q7I9L>9U>STi>lB479L7!} zR>+{eEPN#)hZU+mgO#jzj>RNtwVkM-y0c5U(elW&sjPQNFX;wR2P(}bEM0c?xR4q84heZ)Sn0N0k{8_X7Q^wKZux$)n{q&Dy_& z@^T&w^JQ6Ffk!(`>ExBu?(x%+zI`11CC!vWE6&q5b?}o>_b&QdhTR$=W734FVsO#W z{hPM+J?J!69v1}yQsu^v87HEKf}CdO5y!d*5h7=ybG7zy$$Yf|dBs}UOpS!}I^i$O zCUCO2+0QZ((8pcWRUEL+oPa!mA#t`Z>Rw-WECanjfAT(Si*(XKA9R{)6QFCPV)Ke? zh_G_5{Tvk+%=!zqqRB*U;}u6Q@>B$DTRLACs&{6moZ8g{%3}dRur+#>)wSbTvbGGB z2+QFLY3h|B!gDx3bV8iRMUIcc{y8RCNY`E4cTlG9tL^D+AL5UeU&k(rb@m$ITfx`j z!>+!J&nM`XMFex^-L8&fhbw(oru7|($7{JO7VvZ|=-RJP@YM7BRsc$mEkpsqte&jC zrf(BYPI76tJ+6Z*MSK~q?5q}kmFu${&!_@M$1Lg+@@&VtGo$&nxT`!y)x$~bx2bkG zEbV8gGXj9HW%q(J~M0CP0cnmYH-`7X48vEB|h-S=$+?=Wgfa;l- zVviN>SmQz<_Uu7l5`u&-RyS8tC2HbMM3XNhA$_;eoGRijF2^_)*Iff-R@ag%J^-(? zPmg-eRPG&nhK#3yU6ktHzk9)bYPa>!)ohFe8av62lCLlwLsQKHVt zxY_(wi*$;MP;bjqcu#uSNYBG0oM$)EF*3Dg#w}qvcKXOys)}GXWwdI0YR?D?;?c*B zR;{Qz35r!NCgZ&a$uYtntDMMr@y5f$qYWqyjCwHpoag;B!HO(5?=C3FqCHe?xX$W9 zqR9csDC$o}3K18(?M*v;<1(vsXY>4FA@`L>h4D&|K<(7>uJ+o&^*@GyTi=mRM65dd zf~_$DtG(Y$l*@nY0@<0?Kn#QVdU)33kReO)(6!!V5rT%Q-+MXSvJ+00Hx~_oI!|1_} zUv*~`vOqbS=Jf6}wWc#ZuX{?n_1@TTWgA~7DtTyBQ@!gK3WCm^Dg!#AzO>i;Hzoww zNB;@)aE{79W{oW8LqY4}xL#VYQ1f2d+eUdTl4s_F<&Xs_fWPDKz z*{HagYx2@j@=njfRKDoT_ZI-_5yB2FMnM&2MioxA&hn#oqaYCh3-CnmH#W(v&=-{X z=bp(~*`bH9&Ry_-oDRC*b|PDMwn&=r)Og-+)|Bp7E4lvmMa6=OVOPf9)ML;DN2b!ehm&sV zoOYeg{S!fVa)_J_(!0Due{e$;H9DP)_Eh~7a60NU2oqE6%|L4nU|z+IjuW_ZAdC~7 z9-6*-y6E_~JTxES=~0P}ZzPr<)=gd%7G~p4qN>z2zm_z^N7>&~kBHHKlSH5SeRKA} zK#elijmsMf$lJnVj;~6iqd)$33hPZ4lwzn@R@R#O5)dMFzs>meI@4HzfkOZ33Q0i- z9h{~ZCISM&wg}1?jc_bvy+4VEZhvQN0##N}wT~l8Y3%;z(@Tb6#=^k)$)^&;c{2yS zR{!av^U>;rztR$*DRzC_I)EKE@B!yhJ&UT-uU40l7@8pA9++ z@`@p)f7`P;7&=&c{6FQxTq(_r5Y)q?7(-9VuL`ehKi42XJ$-z5d?9?XpFi!L&_QGN zKTOr-U;kz59~A^KkeIaVFDJjsd;x&^gv%3D%XCAG7@VL`{vUd+YJSasx~dq02@Rcv z9|o12tq6n`t{5((zKnkQHCn*285I2V41A2SKLigMe_DSHf?!_3#6Ni8Up-$7E|G!U zu?2mcV>=RjL@Wd~pIhS#8NX!%1IH8T_+E9_*lmRG9CV$Kdwa(tVPd*YAv&CJ1EG=+ zIgeGJ0wtk(1wzJffKz~lvt|#6`Pmj4riF$&9G3$E3B^tE348=hnXs+Def;7*7}CgC z{K-2W1sLrL@&4cp1X}5ET4(s@yg1|l48_7b7H?ya^(flK|nm=R(-?8NVIS?U*2@3=B z;~=;C8NfN1$iVvKATC1BVT743Ki5B!@cJzUJf?c3=1c>Jc?isLQ?kQt8U$+C&^V@g zWQuiw+~`eQESg{nLT~_?PZK=;#0cl7eTe1qND1v{Wf;?7c@h#5fNzv_`~!uceB=PK z1q9RfJCQ*`Pf{YE|MTwqs8Th9{YrLy4>^GqxdKxi)M#5vhV!$z+Bw>NWPXY8r?g_e zpFX{z4A_paAtWSxxh*RwC}{5K`3LTo(*WIgd$|ecceKKD68A_S9x&*Nq2M&&fY7N2 z)F1jSJ%>P0o3u2!W4xIeJs6U4{~4N=gT0Bdwdlw;&u)mPro67nh-igp!j4cRaFuPV&@1Uw-}qA$+4od<@_`Ru&X=n&i?LBEVL6 zE-qaE3~ZlCp~W!UF?p**0Ywe)lUOfP(f4a9$@qIt`L`xFa?{H0UbH^o4^d1 z>{0%)%p)ygU`(mxfx2r_NN87aa|1wRO<>rik0Exzd5iWM*>>1}PD=?AVgda3Zvy#b z15IQZ>aRX~4<{)`d08e-k?ZJ$a5xsbYx^~WjU`}^DEa@{U~IM?7$9dpV0C(f}qpHiNXJ@L!f-Dkv7ReNT61O4oP6Ee1S$R zpGm^1s34(Y=9Z5f_&Snm4)kW$_OQ~%gD%E|Ku&`P@Ry|ltd)3!_|Kdv2zd+hmIX}3 z$W87oXpRl=!8;xD?PPy%=56zuM~?UWmW5_-$N|n}0+jId^qL3za54bslP#c=Lz3*x zJgQ{>&w>sOC(HlGav0F>KVmR>%Kl?t4FpQ>kE4HtH*ay|fKyCiSbj$*H{x9y zDf}K{_qS##guKqGqZlux<9Zb`kLjaX^blMw57W%&>y?|VfKx7%@;~0lAEVAY%FpgL z(Lkp)gj9_I$+ns2YaWllRwZETw-3fUA1?Y7Xk6r*PuGjeB=OU4^e6qv8%wVDe#=lD zYEe)_84t$sWDH1V+^y28HIH@Y*Z1oQ(g}M(3nVtaV7Pga5qQlNRH@`@$_Jp|k7hFA z7Qs~4B;+JaVq=D@y9zVj-wfW9ipbx{_Zd;nvH7j-=K!VaU2+lsC}#kVzXcfWqvsO0 z+t`4a!R2Qb+lzUvh>Z!4>^S?KYafcYUz_!szFN+f8r0eEOm&>IytcSh^SWncYT6!b z+^f?0Juonk#@L8^e%GKg%G+exlfo2%he_=S0RgfwODNGRY>ot4Agq*z@l8SPcum5H zil{ivOFcD0iPZWcYT<0SFVOb<+T*SzH-_?y5gSF=ZKB^vFn}t4E3Y2msOB*51nA+8 zjzC(mxyv__6~YgOxBB1s?PPL_b;=JOM`m;7+&o*wFNX*Z zb{wNj2M5k`Rw-(5Uy&0WU<%(WLAN-fjOgl}DvkFYOy@&J@>bM>rm7q@NvH)_xy_Cr zjMxh8lkOrM0WQJxTkV}l`uDR0+<`&s0_Qf zwO9rVQ;GpWP0VGR!iHe5{p$k9MQa3&?m8T;Qv(%fe5Kf4&4a=2wS#Ng3Nv?)BOgZ; z!E~I|K{H0&W6XISPA|fM&U@?~#0}7Vg3gru6R_2#8P$a>a;g=mSV%49-q@pkQ>;~Zah`|BJ5(jxIbzIcqxNu99R`Dkm>J#leCuf>aBzCD&1 z7Mn~;E#g5BL%PKJ?$Y!os={R%AVs>FJw&Jp0OPMIHe2)9*U=eRZw!$8HH)ItDmIC| z_Tz)}580daDvkc#@TGYJU#x=5#=aaxL(Fu!pJD#{`>0}Tj6v}>M$chmT^P`kR7CrH z$&6-EZ=9isE7Fb8Dp^=@F7ZvWEw6)CN6&Sm^Yz)GIeySQZ7oRoREKsEpZ2l+&Zxas znN>lOkh`qSL?!cnS>M|%+1PNxo9%5ec_W>kjMCwvB1@yqLTbqsFUMHwlI}kvv>YnV z9WxW&b4PPH{QP>a4&nAYed~jZo^grDY!F91Ek~?}m@6n51g=hE!jAP*cTQTD3O8&) zH#;S-O{}-}VL%W$!=p#iT&A70JrD!wRQ;i8NPvW|vn~1xAX{-E@G~gTRpBy`F2WRG zcXcAU&0Vmg2l6Qs?E1CzR1**df{CGUn!eOqR1WQ>oPVlg>Tvn+?7cX z`!&mbjLoMbm!EHi2t0~!WmBv@XEa~|No_TzSjL;hx8uShgw=wZ2?Y7h>Gt9v2d0>a zoLz{7gk*T}uKG zNjML&Pf(t1$%k4c`6STrf%=R?^bz#)6Z@mvZ7~nwP40+KlFWtA%e0GZr^{ogS%@k5 zky)vToZrpXTlT5c>96_A){t0uDXLc;;(`eS)%ey=udq92s5+|=#652hjNKE3JGo@e zQ?GxJpe4ohQ0Nha?8tgu2+RI-Iq|~pt(%Yz^HkE24lCK=dAY@S*zlXeNso@ z?(m57L8NPjv8xuXkC(&x^p-*C%COgXnP>ack>hTtPJ2aJco8~pjQ-Uwz5ur3yixbIesakRvLD!P0o~UTHX~Fx1 zDkt2onKY{Ns+D93j~7cgN73EhVaQIYGpb*Of;T76_5G@!-DY@1vB{XMQbn9@kI2Io zrKCrPPfbO0I`dJYh(zrRjp5?_+xN=eTsH7!=BQQHEh1D9ePY@8oH;w>7@CNt9J}a0 zYzT#X>w*lnc5_IQMEGA$#jBO%vle}xV~FGU9nrU5dt$Wml==Ci21n=8xH@Lq!Apeg zU-zX<$7d+-323l>M4AdWMa6B%B(|OSyA`hVF0SdSeo>~Vvz5k~DOrweuYeH-tW>KE zz4v>BFsWic%PR&>jV2DylU~p#`uxo!3LhNn;ru9{R3Dou%YnD4=v>OK4|aExAiiQ# zwtZ7qzCb^z7umpIvTa;jOEYx)&I7mhjhbI>x3lOba#<$r9Udk+4{x^Ryd0~|h4yA= z?Vz`B^3=_PYH$dnx2;f30xo)?%NJekflf@gXSzs=ltHf)t)Kxp7T1P(U7TYtx*^%O z6+hWC0mC_!r90M-bjqzSK1E(1O_G|`*ptQJq}h}5pv`02i=+{QxjTJjR>(fb#qN%A z=PB;$cNAYnvxg2CpkJ42yesrF4mrJEU59V?g+hB`|z2Rl9{jsNoWC&`J_gnHr)jgN-K>r+i}%|hUrmOohBxA{>_0fw53oN3G$mfEcu(g zfzsnVvz{YGN}0HE*1QYWtvh3W^;<|^oV2niw0<{^QCNz%>2_>^8K!O8hv8Y!6GfN> zpDH`zLStdFV5=uFD!#|`E~pqUcIjj)Y#hLo1iZBqT?U6tI!AxM$nVM#uLwbD-iD(* z%M=X@ki?zU0o&%XyDzo6itT@Pvppwrv8u-w>Q;McZ6MaaxXrN26kLoSUfcdgXR^G- zWX+B6KrKC0FZU5tPdS-&a>PogYa}{rYt*>9_doPFS!TnT8MDS@{rKLXAjA`5;mP*1 z#O51vuxE{FgHNLIC_bvAa^YepyF$a%L-G^voH*+nN*u}hJx1lZZ+yEaV(T4Amg^m%u_61Z)EiCm=0MKFLN(0 zxt1uZ-I*HCQ?ZzTSZ|hcY*Xi&DTM(gz64_AD;}?o2;mymj~J(?pLo356BvS6wI8W@ zd8LH1tBt5+w6fG4WV^RTG(qAtS)#c+R=!ej1@O?;dSVKp`CB^&-6&iNKGj#xv6s4b zM7wi>+WnN>b9%c4klmF<&L>MMpE^pD5#HOjI$2Uh^=2b69k-qYUkSDlt+btn^rt&Z zZ67oR`Y^_@#PW4Ce_I=O%~Y&wUXUI*Kt32q5f-5ABAH%KM)G7~0t-=E0%1_yk`*SI z7?v#ekdIF`0kSaApr>t>ZPB^Wzr^mGVlL?_yINtrTT!El@W%FH#J&bdO6rE*u#KG$DIr$S4;r^@I*y@I&c-ZAqW|KT}nTMuCnURadS>c)Qg_N!X zm{!02d2mFxIzMU>t2aM@Z%iSA=7N1_5ko@5n#+w*9&r&Kk~$Wf_2RIP zHMz_>4FQnKbN7e=hIHl3yT$gAVXWA!uLEEw-H}S$cx#^|yABqhv7l4y`1sIfxi`1Q zy=Go+LFVn1<*s71AqWfS2VO~ZB7hWy-E;7j=_p*SteM!gs>RD~4pXb9KJD8n;)7o+ zrf_xX#;Jt3KnH@V$Lmvh)rfK$1`?Cum1S>hQmtQE@UY!>o)V`t6G$bGF}1x+pd&xZ zWL^Xz;2!FI>z?(r(0pgRaMFiw`K2suZnZ;0}Wu``Mu=evMfnvZ36B{CX zPPp*Z`m|!9kUH%Cp?P`Q_u)mtIU>wtGQ4P_(kF{v?!e@6#P+kWyTXhcJT{gZIwKuH z#5gmdR1I?Q!n)2yoeHz~y+in;j^Q^v)+2HaN|)l*IRWO$&aRTz!}bfWz`$g-cume- zu_rDQc=0@oZPJ6{7l~R+bFWVbe_+)uefl=+ZbpS*(LDf4j%5=k&WKG8FY_!~8BwOY z0C#rMRU4?>^E5t*%yXr_^Fw(9e~>7g%RRM_&mBEXxBp`Q^8V~zgH2HcueKvp568z;)QNe^BzINCMEB_@qk<6Tn1AjoKj& zJN?4gsZ&F{5L5hih5y6cW*nYRC@KJP{iKfkfneA~bRFC#NV}rf-RT7(thz5iIEa7Z zjbHDpH|JCxXvsKEL&pS>?Z7%nL`Z`T@5b&x<5(nvoEd0YGbGN39%d6m06XVv zAi_4~m(3((=$rdwJ*)wH-^a)2plTT9nV`ze0>51c`mdnA8Rh%zRO1z~W(w2X;EeFD zRkx-$cbKq-$y5ci(&A zcA?Rgk~eOaNpWdpeJCSnFz>M3=OwnqRSn)z5!Jw#Cw|()v!}Q<*9P0=Bbo5dK=G9t zqS5egE0loq1SfTuQ;{2UGB7N2^vO}F%H<29*zqDhvi+*JVcXsW~tw(FN@7AGA8du-~YV@4f+Ny^SBn)|6r^ z{P0-j`t7&@jCtWyTsBuhfX&^iygwrehIQ}W;5#Gdkq^O&U zASG#WRG3Ok!QP>H{Bnv@;$Wx05Jsf(EtwEAh;(66N{JjpA|2#n3mzZlURb;u804$X zE)nQ!wCi4DMhu8#GVWYMSSmp7JbDVyj5fRb9up7TzYG$Yjp^-~xa@7vvH8lq3J2MeioCG?-WdVJ zw*kJD5#R89kQ0g`vN%~CfydSEn>joBF9E1YrfFVidgIfw+)d}c*YS~z3RZRb$6^3$ z|8zyn;zFY*>6`hB};cwj;q^jXYCn18b`W+fYs<4lwH=;y|E|H@j zFuuQj(EcR|lZkab`PifX#n5V5ZJg6Wf2Sd6YmLLaQ-mAxfOYsMtTAC=YhWX8XZO2L z{ej1OOI7;Ehb?oj(EP?*{FQuXM^&^x*>FLgz#|mIpdkj(02kP}E4S3KmTdtp8(r&{ z;XGyY%v+){AQPlrKofSGU-A~K_Ah0F!d1C+k@a0FL8_G+Co7JSDNg~mT&tZMB3eqJ zD}Kr$8f#;DsV}FBhdgxRjBn=XS9^`+S6uQ(6zhbEki5!4sQCbist2;`xPj((ULP*Q z&+fuyh0_mjwp$JaGe&^KmYw^hp*{e$b{@`!sB2*_9bvJifVpsiKvrp5Iv|)zz-443 zw_7vOW4hQ>KN?{160F%oEU(5twokh8PBy@`*xe(XH6vW60ejpB;lLcS!GQB9HTn zc;vA=$c*?vT+|MZ-M*(O>dpQTV$RCrMLuHW!)^RHxHxro9p+~jC@%CJIjiSz8~#)d zL=#L3za~tP^yP;Zn|D}^KfCzK`_TIoTvrU^W0|&AR;qScHoVbQ2j6U4&7^wjESj^P zHH?3k_ef4_f13hT&}GciUab0KtA8U(%q-ba)a_vB4lrB==ZCC4i*> zC>7i3db&wZpViOQ>HEqjRD*7_Jb``bE;us>#EF@h7A3eBPa&t(cf(xe`h5K>%apl1 zcXcc>@On}N&68|uX!u`-*KW0Uu*2~D+36!xug3Z*Pulgo*V*b@mAE%`IPaC4X`4Lp z%lgh2L^j#lBhvd%}UhSKs=reoZ_4o(?2|BaTaIZrJApt>fYt zIR!aF&h}(BU2u3T{UV3$CLV2-Ga>Wl2JNLSmyj4P@>ga@PTpiZa<3NgTF%=zWrT$QG zr=*;eN=P)33w<-tMV(-+j)X>p#oBj_7G8GM4zn>G=jDE3$5PyYT~pF$yja0wXT@57 z9GpX9a(3Owh>T`|U`c4kN&=6!(#iJz&#Pv3Behy|meYkX?sdH)83Hpi8B$yGOGT!A zzp@tgo5ea76z1#RqBCX7DuP+GzPx}ji8xZ77zpFcOCrRCnC?%Si59~ zElypK1wH+p4)S+(T?k8F{JJAFIY2?SAf5P>vBJAs&9!c=C#qtu25mn7b6=3%o7>9z zHxGNN7Q)IJzxr6=R+ZaqTIZ;f;_!OSii6FEr#`BZM7IR%VJ2n?$;We7UAZdTozsdue<#!`6W{hg?m0L6ZHGJcuK3IGQuJA624zDe%qfxCWk0B{vL?s3m=Spe%n7I@MG()JF8 z#5@#X-l!i?;kuM8G4&&(K!4KT8-P=ZBHS6lzipa)5a98yJrEWBDEgVU4*XDB zFx2Xt`mrbx0Dg+PlEwfKq${v<>n7r;Cm;ucW-2(l#`bEC{IudsN=FdYz>$N4OF*v- zO0cgC#XR_NjKW>+4xODNf8}?I5!z!WO*_#1wiL^lsR(NP zP}lz3=lt$3oMGB4eu|#}<7fC2;N*U3@tXPIM{pZ0E(#PXR2CS4{N^F2SBA zz{xaBwV@y5HqJ#ZR-OF|G6b%bouQjsB{yN}kyY5jrpB`O`+6zFJ64L_d)9wHb8 z2hM_y@!35;KVN`x!ps5Q8B_JBg1Ws z{(X(?4=<4@K-+;`!rDi9t!vISi^`BOcU*$bvhDW7O`3Pe7n12^lyi z)w9_k`Wy;{zLb{_&M8&Kz7Gw(Wo+Or_4MgpgND#m@Pje{_Ey=v|Hjz| z>Tl?r-wFu}AgZ4N2g)Pk#?c5kP`)(%xaa%=V7eN=u98~;bPP4vA>p6?z0A0cWGnIs z=P(1}-1}s9O(BLQ20ZtvtMX+$GU<2V3&l%cW3+J_f368yg`S-#eRT?W8O2Hf;Lq0p zfL0@lCKGxpdi!;9Nxdd82zmtoyH@eqO0+Bo4Z*cwqP#gtYq93$mqToK@4BipR zC&Wo3wP&U{``|^=lYtD*h+43ISe2lR=)@n!Cj5ol#t}ZVUh44hAlCl}A9$P5mg_AZ z(SOj}=WI%e&w8UK0bt7vwz z>(}XKW@dgX0pj|P{RTIY=Rtk`WBJHfa9QY0KJ+OsD?r{&P12)&3-b4}J+R9)r_->IXGUnUi`;Pydf%f z{cPXlUr7*n18vf~VXXb`zwQFr94;jv3_bc95!2x*WSazibK%rY;UOTtTK}gvh`uVL z^x1r#moUWQF0|nlc}l76efHJ-_YC7o8_utUP+O~2xTuFM7BGPh!#@iG=fUR}^B!Sk z2+fM>mhIsvJbw-1@;|N)rv_JC`0f=VW8%iYR*ezo>6!mIV}8trnxZ*`CU|LW?U(+r zF|WYR-2R9aFZjcO*nIDojFW~i{KuY%BqB6c?Ys=v zKh-3h2SA9oDJdTs85srBf=P7QvVU0jM_iYS{)n$1Kiuu5HM$<`}Z5GNLJ4_S^s-q{8I5&uMmJE zs+8Ppe71Q0y>HU_etI}*A|8)egLw$9k6)cQuSWULd4rtFRbB6n*isnv?43V|iC*k$ zx1X{)9e8$BFXhA^PK{sKMeuB}?iQ+NzyI*$(R-)oKAW7Fl++wAjVf9EPfOX81$&@8 z6vji~JtP+$^uN+N>o4G;f&B&j6KEr1@kD{QFa99K&45!Ws_@~8OT4!I<6!`9-Cu$M zENAQp41P%YLP4INv)xCj|4%JV)@`q)3(E|ZmK*Ric!+!#3 z^}i1}R(1cVk4jD@!J_%E1=yi-rWFv-4)#V? zR(Od2ds_$kYQhX6iSbXO3mT5uT6|8V=rv9_#GVD@=9M+vcgT#}A~lUc{8*3ZzsSv$ zbA|ov5c;3J2bLFT9)=$`GA_Fr6zYb>W{s!Mp_0kxKJuz6ci_RWIbR?Bs^3v9k|(j$ zT^DKl$X-@3uXg*t+8j|kLNc<0s%hw>y|LyJE?>t?-4o1ibXt9(c=h9%truAnTkUF9 z1ccrp3g<*~|54GRenen95ZP~^0j1dd{rghW<)QG#r6mi_-67YfPoMfl?G<_ul`7k< zAi1|NkP0sh87YH#onTpMLA<6mv~zxjMe)C1H-FI8kVfNy*{sUrT0mVd#*Q*TgxHRy zq6dZ@V*FIM$U9=%7CjND>g zJ@FEQc{d(LJ-=4*-}N_M)af24$UFvIaq^s2Ye#*^oNw-sr`n_!Cw7Bk9NH++Gz66% zD);{DQgEgTMsJc)lmRc|g#Hrill#-_hm)YWVZ)rdi(Mj-oMcHr^_&);e-63lsirsn zCwxNac=YV64^tftq)>0EG4seyJj~GdXjM|0{li6VV>s2+D>T=ygIN{wboj{z)o_|N z#wP6>_9C4k|NZz7Tm~P(^;+4J`L$8kx4kL7VftXDciZ4@__el0!RIQBTj(9LJ5QMw z0Z+Ry>zS7ZBzk&Jg@L$)x@QDJT!iO!sc;Rby+f9R7MP zZgY9m8vilNgOH9%n$PHr7vQO)4wrSS9MnB9YaAU7`bHY}jGM1;8BN^I)yvCv4?fTS z(}rIJ8d+`iMn+(dUd*lQ7=ZVnbuGg<4-7{}2TW2&G*-R#eA-!{)3=4qRset-5q`(r z{3KO6pHP}8F#Qd%eect2b|*QUU@_p_Jtfc#VDH6GvnopZ7P#yN}jV$^XDP&>SD{;7}{U>J1{=csbC>h=? zK$eD~j>w|5ry z067>FKZrc$i08}z{0vJP5&byL8|qf0KE#% zO#P&wc(UK_#m|0(1d!V^0(elInM%&WCW#E0JdL855q(?p)r4u%I96r@HXT+lGLk3k z%Xy*kf9z6%{$n8_p?NTBD+Aw}c#@gV<_DhDG*Y#wG`GpL>nj(I@CUomL%KOYat^aB za=gdyU=pV=T@WXKhg%@^#dHYJz?8R1@cl(qFNhDl1IWDg%r>=oU6ahIYycZyLB%1M zo%a9FasJ1B|NEqUqdiqK$b|G^+9dR()}52JT*IYV`jIb(1N-wa@QEV8gNsj}=BJRz zE4NK&LM`|m47W7O?H8fX|6_~{S0&Au~E&w=Y7j#*5cw)9HH2x{&#)EJJ8q-oAtfW?z zRoRip-n*mi9!MmgEwwv3CT=d5B_^a})+=vw>rf$a?(3^J4!f)T{N4w_V1j_mdc%ni zpzn$UoPSDIBZavTy6&#aPwc?Ni!nf8zZ+Zv*qXKYowuUFU`q*yi8u*RCud}ky%D#i z6deURMg;xyy2D0G%!`gby9fE~E%jv-&jp!w$9+7+9_;W|3`r&n@MKBPG?KLFuIbi! zC}sUxFv1-5$t<}7X<}gqMRJ^A88$rZ1AAMnqBn{An`*0ELHC>>uO4)y%KM|1ij-Pw zX>5vwW8RHl_mcbW?b`8u_y@zHuq*$BpyRAod}Y6P&b*!dHCO%oXt}AJ6VYpOUog97 zT?WxzkSUTZ-I$|YzvKQiZyineue)kSazhklvv>JOpt(?)Tb-$})s^WtQ_O^n~CGl^YESQ%taILsJH@VWo zQ>ZfC9Q{@)nk}!$TO&h~2es^0fmt8th=R*U%^MUwDnjoh;qGtAff=)zK6U20ZMb z^#elfsTY3W)T{+dX%wkwlF$ghSQYR}6|sk=XBeBz<@7ChBzBWX9u5g|nsrLE33Y5J zzoinax2kcRgg)n0Lv4dve8RwAf)$#R#-&x#X^jVmS{0$F)S0&-3GLl_HRzT~mX-sr z4Efp7X6HOATxlij{1n)R!Da5w?ctF3vn}nf z1A^vS*a|PmYvRoOo@JVg87}Go&sogbv|I>gj^rS%U7&-V7AypRrIpS)eA;~nE%jJ#ghh`VS1 z2&`h*xT$#~(|ED){fgsF!>~O=izJmy7Fs7htKNf`n&Z<22D2c6y8`DgF>|{@5Va+e zSDx#N6iDc+s!v&oo!AI>)Ehe+I{0et=4UXPj_vZk-=nP}N>$tMYY`9XSF1~N8|P6z zSqwVak8NmXdqCFX#k1-DNECGsPV{&?((A=a=^ZYMP34L#5?|k(+CfCt_;#UUj$!&x zM}b~)f0%cJQNh!vHy-zR5)xQLi?S6=E5FqayEg%V$v{l~0TOkYt_ekq>OHNIZmKNX zRK5_2_s6?g$tuiBGu?@G9<71K&3EEF&;`f_&qa+Y4k?nNLRZ%a2+~H#6S)(A&{O7N z_5!l9Xn0(!MPlSmi|4Nfx7yh5{oc^iPM2;drj>~e&RbU3^TtI}IjC!fo*iL{ZV{eF zA_WBdes*Q$3kD7Qfg!sGtA|rNmL_Y0rkz_w`eCs%2Gmjax;FBM7B>9s+&b>7)y4))z*wekH52p#-X)c$LmYEQh?kHj@WyavNZk&T9;|>M3t7y}Q!Cuk;=P_& zRgO)~kr14AD=L&RM_eBWG^0Bn&w>7y(RyTC-oYbw0f@xr!r|FYaQM;R7Zj7S;yQYd zhRmmSrBr^}u6a=yU_muE4)rd1`9RmZ!X-6`-6p)s8!j~R4sFRU3hZ|-GA%ZD6qVZx ziZ}9A%KgIyP$a&;RczAdm8#iO==T{`qqqe)>8Me8!+)_V(>C1Y_ZZYo?42Z*6G`Q` z$rA1Q#2cki8tq%`P>a#2 z9fn#HJ}bOnEtr9u(nvL%@ZHPyc?C-!it`|E4fJ0|Qk12HR`gu&KL!{cxf$_*9Ubjb zFRl8MnLz&u8~qy7ZP!T1bn?lD`wEk$(GHkxH=nywR(pW|mzCArwC_oiS9uLF3i9Z) z{%o$zFzQ-$aglHmDM8p^gEv{V5=r^D&sN5JdRk^-C-|D1Ue950j)+|b;-~)Y`W%-0b$u76U zQk;Qq>`<+i+8wM>zobGH>*(^Q3NWh(!=mn?O6~T^@6=r(p*nb$z-OCXq@FFQz+;1) z$ek0cvFd^h-EuDaJyz!#wzD)e4)fF8Sgz(W-C}BtJAWDookn zv{{u#15$Ava`1h&G**tBBO16q+(Dd@Qfm`cIuyJvb-SsAq9E9?nabdWj#^-YQ#T`m zkL-fc;Z3`81^Uqy2WVdS6ZIawYNz-sUasNepX^4%F>^zj`VJ3T#;JTqbF1C1uix<6 z6mwV5vE?$Ds@*!I@ol|UFsV~+gC31m5~1Me5gz|K%d@Ts_HnjZDOx3?r8@7ekJnDl zRaJCu`)Q~er2Ph3$FzNm)yXr=`9|7r8$=M~*ra&;QH^C(GnY%LaqrrZj|bRSoS9Mk z>q5tOb5q|dF^0x*nNp8P38zAQ^}*;5Myn5ct~Ba!1CsTy66!Q~^&QjwLn^M6j&qXv zc(I_8fP0uRzlYtDn;XKLimB{SfB8c1{llsYjp^=jqv2#&ilK@U zmYMy$Ec9dIqu<8yBHP??8By8|Tg6wXxE?58Q2%6*>)_1qegI8!T=64RP2hoDTTD8Q zxNcx7tCa)p9lC71a}2W=9fRgWyVkfp8~W5Xbeei9mAp5QWjZO|Q)tH_i>8?AqaKf{ zk@4f=AdU&k{w4Q2$KRBz4%chCs`F@Ffzz?o#}w6R+)dd%@J%2UQ@RmW-ZHXi)&BDt z(P9T(7PU(bqFSjyWtm;t5mV?+v4+5Hv^SRI7iw{DQG)=)^6xT*A^YX@0M#7j3EwB& ztJtesZJBM|VSE@c@%Uj#3g`yYq)b`K?q+Hc%uMMxC)~kRI?bn>UIs@o(SG<@R18CRXY!(^%Lre+-)E_c!v4dq6dRg6 z$?LHm`N89#9LK{F=|RkHvvIQG+mWRfx6a<<`^2HLv{#Z@8^V&W>|3Z?b>G(#XBISR zu*I?KM;4pXxDq7iW8AcB3T!v{vUElihda<~nRxyHw;kotBSHh0cmDLeFI~H}LYGKh ztxBkf#Dh?`VfwQMwpvKFI)6MhE!7uktoX+I#!q1O zLv~@kpTGQGdNM6B<*7+?m15D~MHG!{0^HN(u%SGS82%C}sXLce1m_Umch9DXJ9RN0 z(mvL{x@AqKal2B%X5s@U5;d{`49-9{NgSI*>ZWy~p0NDITkb^PsqBq)6Qh^uL%3@9 zcW;m(RFtf4OzXK$&X+$>nt8>CdT=})K(sYFTX>kE7P!mxv!LDv(82pt#JH>sTB_h= z!YyegLk8t#Mfa97nYnhbGwgYb>+ZurnqDQUNauHy+6Zg2o+Nf2D`<(n&7q6xs!)L7 zZ!{d@_wa`+V+{k2eB1XY4xFDyM=G;JYlV#9Ix%h6j_ zb=$9erpND5eR;04P>ETWRY6MAZmWv1$=%KeZn zZgEv;35;D)sY~cQep%hp;$7nqQ#;j-t@Z;B(~et%fcyT|&A1UaZmZQ_KY|?QT5TQn zTCc7hzPm-35&^OUg86GXJ~@-#6Bk`%E)xjHBP^e(Z>}tZnU?C_(V2PHg91TpI<-Gn zps&OB8^NjKKEkGLspiGnp!m(#y#LW)j0Iezi=+v$Rx(ic2d^4oGzetoa znt5%f?$dymvnFer1}IBIEjzgJNdGVi{dmCiLpH>nG-ohqRroHGOBd zl9KVkn1*CQm+*=M%<(J`hhCl~rGYcduB zl&fli)y+V3XCwt#C>5U=WgB02B7$L$V$p9CaC4|u=wGN%i)7rxK@!&k=vBxS1$n9h zOsUVj=^X)+aCEpPup&g??OJGHw*C28r{T4!f!p=cn%cf5G2cu6Dahe0HT>o2o^8ne zO0W43H`w&sVb-y%+O#*@r}w2OU+s}p6$mk%U00*kI)zWRp8WF1IxI~9i)-354=}Q~ zI+Dc}oE=X(tUR@s^_VGneV7XLZPs>wHTUpEU!HBL6y8|*Ub|0}WqX2CsJPq$=7dS! zzfYL37)3$WZLnFjvpzn}+Ebl2jPyh}n4;3~VB`;!MK9G3jnu*=;DrEDdvl?_h8R5| zB^whN$sjA8tIA+HkKnP{*nTEDdOBNVcj!b{Ln$ET#|M$Qtt38t03>Hnpnz<2Ws!{R zb+D5)QTdkE>0tDWoyOuw7Mc}Llgs@~yP?HsbJ3+-*i`;?50I+af}*uANhNJ;@Z~%2 z`Kr>^OW3n?{co-3|ND!807nqyEI);(z9_{^G!?IZrD^MT2f`SB5U(Iv#uCX2bM?td z@F;qk+cMXF6JxYi&GRs8Nn&bbWWdR9ti(L3uBIH7K^Rg^&qJ$QVe_VTqB=w+smV22 zyUgUEREDJGT5*OXgd!EFBDsJMN_T4P>($2-cr5RuSA^NJsM#$;lc3}tRv8=sByn+t zDT=WrH$&jjT%BtcI*+49Xt7Hy?>!$F5qxAxZZQ+ufIL3Nr<}6)k#cd~D1bloGYQj?2*8JO) zXU8{Wb|7tfc{MUjGdXsdvYURB-IHBu-S4wPS#5unqKGGju>|v*OQbQm17?+M%fI`x zOp$|1^YwT6C-jYzJ_6V34yRS=7s|!)AqyI%nRq@my5pnmd%5Zr`bp-w7pfc>fi18woyM#&!$uNYYPEVJ{>3Z+AJ*ePH6W61u#^_EF@1&B>u%tiI$_u*YU!U z%g}oZqnvbl!#_0fvHv3q5ul)#F~g!2dV53Ua+&h+{Wd)#wwtx9lMU1J?mR=A7f5f| zJz4zQba`*1HIeTo1F2TbC*9Fy<;7+Nv)bv+?mS4$NUATF3wlG;Gw?1;3(47KExhPc zAGO99eLS5teMEP;wj-qjxJ!4Zia!ss$4sU)k1Dr=0YPot=+y1!s-btsOt;`P9oOgE zt83SRYvR$m_wAj^!Tkhwj(!sMeXn|Gj`Gxulj?;=fJ0%{tdD$JeFwWU(>envh`0mI zwOvW>K6$V~LUI2Nw`sIzvDwgxNp~b5{b{=&Ky)**J?7y`Twbo);f`mAM_$!ro|LP5 zhNi3IboFoG?8>%D$<|P1#ecl&k2orUhicKxel8fU&3m4e(8P)Ae(Ri<2~w%U5+vh* z$0-7gU6Ik~qO~4oJy>Ev7s$6;TbBt#zhmo2E@CTYm1;zZT#tracn&sxJ1ABE8VMUJ zR97=obgi3ON|R`i?h@#!ti#NGp^FqK!_W~7wNBm{&YfU`aJK&2fR!oh`oyt0wF)w!wH)_X?~!s@Xa0PkQzU`je4wVk(flr$0ySr4Z18@a*$h#mG)l;phYQ?0XAdR z3JLYST#CKcA5YyIXpzQ{Td?NzaVb%$T}vv=cKGPo*wO5*3h%ArTW@oj%?gaB-Pg=3 zmx(yqZCiBlyGi2)hkK70*F9p@vK8YnZ?8D+v+_i`3^Y4~A&_Qqi$mZ1ch$ULj%deI zqy88+tg4tKuv|{v5z8J4W|uxmQnHxL=etg(%`;tIaKWSJH-?fSB}v6Sm#xcRy*x~fEKBYuRRd5=9g495nf=v{kYJPkN6W6&P+sx zRF)MQ*_`pqD^DQrbCG1oN+gG^zPCaxC>2>{%*9p{OG57@HcT5T7F0Se+PD|0L_IO3 z#AgIija77PLb0+pP$L|7cs%kuQ@BSezt30X8A|B6{r>fg`S|nUU&>{<9z%#oCR^$% z7c?T6X7Nc`9^6niCRK)#b6=yOerq%%Od3qjej0Lr926uvQvip$^~Re#r5Es*Xt>^| z0KwyzHE_H3rJi3uKFAa#@MMao(C|Lh_gH>5>#_~F*wI_<)gnGu?OvNZk$^va^0T(;n3y;#UI}P&&axLAHNi@dmd8vVL)L;aQI*Sal)U^%N zy`P2n3zOfm)SsDdDIcXgpPu;p zxQ(0-7278i7LSfE(VL5oA{=a%)qCvOI2tZgy8i@ID(1%)C-Dt!Y_?Z6msIF7K_XU9 z-x^fH2zMWU=ym&iFjMR!bdiq5E0O!K8yP%>=Yn{7Q^2G(T+Bznj<)-O5UL~u=8BUh za3Z~2ymh$YdppkKNTkxYf|trdV>>~oB0Vdf_nO^!DAfYzVu-(6jwgQo5Qk##tZsIm zw=eRdcI9kOc^;<9Inl*tWV~cIWW`fyqoGv`2woWHRN6j`xQxvMxc>&9_&XlDGVEV=^z zh(Ik)SkZW?3qz)p7-nF7?D#9*t;%1CG5v)SaJ;Gc{Vgy#xh>Il$IdfC4`-%8r(&+^ zt%AwMG1ew^e(mzjVAhkBpeLvuo|l7Am(@~ND_tcVqqst0lQB(uxU17pXu$TuM-&6a zqEf0zMgbe9bR10@f~wtT$Br5o8&17+^v~qOJTLkkr$_r%@Yt$mOXS4&gh?GS{5h)! zu6X=3#l5<)jIs;HL*oL*hSZUK4m)xX?T0JbPNjbIQB}j$^g&d!!BAL9rRBc5JS`fO z<&^v=3MB17!35humOojE z!OgcbkZvx(tWb)CB47+4E4I?p(=&l{CPVle`ebh&n9=8bfx#>Z)2B0KwVhLB>_pSD zopeiFlC?seo76;eaId0;Y$dOPSu|4BPV0tj+S3|z5(d`eQ+Zs!CXS|GNy)|ZkA&@% zW+&BnhRpuQhc$rll)l^saRpaAkAD12H^#sI zR3ef665?!2@BV|$&yckKPsZ>*J=EOWSFzdGk&7OA&48{Nq9&@J8!1@df&1$Sa8k}S z_05_bboN|RPyL{z5gtJ|6^Bpw9g>Uy(JNvV%n_Q$&d`~zlnM^K@fd$cIN#CHd+$x= zlEkx#sbZfdNf=vcp?;vh?e5Awpz0Y<-J4>0B``sGriD!>E09bZ8BiGU-4xn|or4;Ax+)d;1ieh~)V{KS*4^`AqDzd>$*>E!D z@nmXkb*!YVb!I~jjBEW>r=j=AatnHgYxRv=5!KdI&6YMw5OP6g84iljI@M@5iPI6} z8;;2wAGwyV=VF3UDiO_PGP#sEX=$xzXXKbA0QU3I4|z_6e#pZSTbY8inz`nJkKsOQ z{UC|KbAcu63>VA{86pVO#{Bkc}ga`iR)(o0-N}pl#mNc4w8nt`zcq|1QKwJeF0D(Q&aS%ymZ>`0<(E<-?q;KARD3xfT$Hl#S{7t=0jic}&gdI5fMV$tl?zP^9!uLsko<$;!i)SnO^5B~eg+83t5;{$;id;~ zx!XqmA9rsVR%O?<3*RCLq5>kJl8S(Uq#!7*0@BhYAfc3$bS**@ky4S4CDJ7z-AW2b zcT0D(Sk!`VUU+-I@AGUgo_Fsb-?5MF4-XFIT5Dc&jB$=S$DA$*$LCR|PKh=#t_a8X zmeg@NKLDl0SjjOq4Al0Y12><-a|r4nZ0M=)SJ1528Ju`tv`f};Du_?ZFYubs)QjYxM46Cjy{?B&l)HASg`TKCGTKs=^pG>!Uj zew+Hdc=0U7LF}-_*tIm#kQ@%yI zQOY4>icxg?NNalU=Xt&8-5tfLWN;OSb20VmZOZIL$4>dWsrll0Hz7dO>M!U@?VMe) z`5cX25T3Z5f9G1BOuXxirDfwOdP%7Z?{4>Fo;TUwRt29c>=D$; zlbLt@Tt@`qbkBZ!xQGioVsu1->0^3q1ZZ`np^D&cojZEprXO6&-}la;P#krOb$b{o z9C0&Y;Qq1fZ!jeTQD&YpDBTQCECP@(vXo5ufvTSEWml;(+f_dqQ!Ro2^cg3x?}Ny$ zd-K!S!1KpUT(knjPO_2meaV@!q?fal!+ZQ+4$}aZTZfPM3uCye1b@+Z{vKqIu{I-$ zd^1qju{86fkGwv^$IIU3@dTcU@>a#iOkvVL*Ia(9z7;$4LP-}@kR-Rg+I~`DP^Tl% z_Y!g_K8$8E&xGor@Ufo^r*i)}Is2PfBy!Z7s|!}N6-gmFbwdCUCo#jTcxfxiG|g<) zk>mtvca$`arCxe^gM+OZez1(BcZ)QR*dRhsxQG7e0g;lA;teiO4AXt?X9|CZsjB~7 zX`!`+0_JX#crcK_SYY!6{o_g=1-)U&)}# zgi0KVaBWIE3-dL~3)p7FX*{?tre@&r%YyZP2JgQeBGCYpD!n2;5qD*66b)oDg?Unc zpF;1_LmE;%0%OW60gWT%4^f}E{^&sep=wn?^=xoWA0ft;Z#~jq2l2?BvUGBc`7nl= z{GFI{x4X!jNvV}epY?0UZ>$FqR9XvYzgLx#lF|o~UN;G<*Z<6=VBfFCXrJcsuo|0w z5b#2VHCqq^^< zds!KaUYOWWn9h7y1K7o3365}%JD>lUJ^lW~yQ-k$()QO?TJCFQ`>IMo5V7kP^}bTU z{;tFO>bs=)s*cyw1ymNPnY1}>D)~D16F&O$Ir4w&e*y`l8a=c!<67}mRlmvW?SQ;~ zmqNqwH+fx>!rF18l1XxXi`i@7kzv;P*hN7zx<-CBlop59rXEjN%>neMcomN*eZ zlECuu^G6RNKtW5iV}*bYFaM5K^2KxS86L_Wud9C%K{SzOJ#Yty9Wx!C8@6=L?}hw{ zo-2ZX0c_5D0wZj_x3(nRa7Vcx?FZH)FhTN_|9c8G9k?0dKg_$mzX~w%BA6*nRZP@> z)1F}aD%k}L?@0RC9Jd@W{CmXOXf-%@_g>Ys+qC%cQHvlMDlFsCv9 zT9jfWv|=Uy-R)*Q?6@FLCb;vlj=t@|m7|T3rV+hJN=ll3OIbhVU{=ZhpfD>CjCdyy zX}Ux;;N5i*WIFb8tp};lA{#mUJ=%D)-h<8cekti&RqVDwd;D67@Jt}n1BYa+nT>OJ zJ*0@x`1F99M)=+MarO?55>SA8#Ld)~si|8aO}N%`-dk?~uWB6J%0S99_2;`L{ zkKYWrar|pDNkbEJ4V<+pK_fK4>R8`|0IS=Zl_WWSWR0j1Qjk~Yfv48nCJD{NxNj_WW%SCTxqGkC}AVO3Sa{jniY zMVsTbf|lq3w0U3(gSAi}cn8$yYP_mGbtZ*tK|!(ik987%Va45mtim6R`1x1@bkqwE ztsw*RkA(Sfv;4nw8BZHDhiT`uz*Ui&F%kOQ5AFn-9&3Fk7@+JTvJSxL{9|x#1zxEB zai;D`0u8!f1M>KvSOsW;csRUO5ELi6Oze8>?&4h&0|w0j<_AHN2QXc7?yNkT%j-W6 zwU-?+C}Qg`ADK0P9bz&Ddib_;w|ytQ&0JQ%;3<73TG2aaU#<``Ztqn z-T!F<6P#!Qc@za1nX@P2vPMXO`Gy>cCo6^N*xAir@Cvqho5%X1Xo{d8lepVixwT`} z^flz4nW=xB%8O+gr#!|Bx~!5##>l<_?t zX>giFJfp&~mhK^P{S4R>KpY4fB?g+FlcV^*;iY8R|DW_ycU!4YwO_IfYu{Kl`$4SC zOFZi*ZAP6Y+TwJfBIS>28h38Fjznpr;Q+ zYMjfc<%XSU?}s~frjiw9P8as@mf_<-T1g1vOy9I;XLEq_i4-jziz3c@Q(NUJb(?jC z=mt^R7a0%FTP7N}`%ng6;vaRZl+-M6uI&q{_91ux9kJ}DYx!&-um5ZyzS#W-&5_-1 zmJqf;>i~S)V+b+F^KgR)iG(p-3X-CNq zfYyHci2$vIoE29|_nNYN62&u1teHpRwbbS?Ph0uwyFCHhir;RT?Qn0`tlT2tvJm5i zx*%%}d4f|uUmI1@6MLiFrT|sQ`Q<`a8a*_H0*bhEqw_z%y%25+%^XCnsF?N4_JUh+ z#p+!7?#}n5X0B|I!#k=2D7EFD2Zvlg=-Xd{Vyw?lj1`3>DyvORy$jCdCQ3d5b-tIh z+~2OQuJX-bU7_7K-FaTF6M(b?ry?f%c+GtMR4ct{^)TS#4V{c|e=0#mLY#niUM;K& zy{XyRGZb!OB|nR(&$lt(T*9A1 z66GWGU6G(yG&R`}43V;K+b5~bR)6_+E zC#)9=mcF>4smm#66m05)kZm$sQXhK4pfNQc>Cg{VFV9HMccr4Zv@wYTxn{l8!(&;M z2OUjri|rDVu`c7hwkkSzgEXqY=?IocXT=V0er?qZjC59Zi|(aVwxhXe?{#!ZfziMR z`Ad?@KPY2cd<^31Ux28I3*-pwX#|X^PF=>49uD)#+Rbsz<-EFUI&I(cY zLu0+=X+O378J!Uvk%{I~7*i=2M+IevPk0hQ{Jr(QB|r)w8hK;vVe z@g$zLTsMJQ0kjfhIxWWenScxB&bGn5gL$FgN@NP3$zjoZh6DZ)pOPrFQA_K5qMw|u zz%W>#nq(iin$!3~LY5!M&&k86`5F7S8JECQwS&7X&L2WxSQM#ij7c!j>`RPup&M~ zC$~pnrYvsY=J*S}b^WI4X(r2o_@upk*Kd!q#0?{H7$3(=WQ}}rY*2L5Z0AW=1Z^>)_bH*IvH{u6%LJ63-2^aXHH`DUOd?gj9GV`cshN~#;Em! zsS1({-Tf-rBm2<9!Eh_Ocp3#J`FfJS?<+-BJ^vBD_7pD#E1!nf&FIv_9**WBXuwLb z^5Ew4iMn%6>FzWNoyM^Oc2W#7+v){NeQ~;~ajs5t9mOvyGL!0+=^BRIl4OzKgh$>Q zflRMlz@+bNq7t+(yPft>*1F_rH}9^Uskct~ZfjGPgoQj#8bLv461UrzyH5SN4oDf~ z?N06=F)U&BqxcPxJn4w6d|5yYMOEchrYWaglNV4>jk?S2L%n@li#J7A-n1u$Ejgdo zc4YtMbXQJ9o(C$~trV52sp1lA-glp{OSu^0fO%S?!oVD3lb(4=XB^F0UJ2&3RHZOh zZ9{>P0!tp}?EPSs&2?3`T!EJFP5SgS?cxMFp|xjbBypNG+`39$*u|QBw4}L{-K$43 z=6Mpg*+$EKKUTrJgxDQ613Wqur7cCbFV@Y+Kt(e}yf)>wM0|X_82?Ah^a)ZH0nOeaUJ-Pi^#Bcj9l~{dGPb8e&)WDc%@$4; z+(Kt*mok8EHTR43vFcafrP<3I=D#ksCM_r#T<-LVd}&M(bH^bpE0n#LkOxr_ zCv#D_&Us*I3(U8?vs>Vuo#`HUsf)1+~*=76BS!~>Ed07&*edMMOx*Cv?@r0S?Qc@x*cgwOuS>IvpY($xDYF8q(uf8GS zMeZr4lTV#w{mD~?IM{jHm8R7ykd+kVmL~N?Vd3H}3uhzlu|i#U`otS_w+X@6hG~ye zU76)@WQgKs+|}s{DhgBYpC7;7OO(sh8A8=}}PquV{4*2%lFwAXiB zZ}M?lsCHJSF`0L7=9!jD3BgD6Knft<1^1SNvW#{uURBX&K^N3JG3n4*M*lf6rDVCp zmGo8dmwa_I-`R1mx$SP&eI7jT?UH}q4h3cg$}DCIX!6gyIrR7`F*3;Pt1}?>QxO3? zL%FX^GpSSAyw@EspXqicQZhKX zk;r^N1*M>bJjgaFr>4}*GWkW94ML>Y)D!F)OXM4vD=A`C#l66lTu{?35K`P)>}NqC z%?zmr%MJX7+z&C+6$9!Ot}pl3W{|s_Jc?gHglD+^!Gz0kc5gRFU(B!Zu%Zk@FpGPz zxA2mTJz{CcYSKzUFEs|qAglE{lB=tG;KlsyG@7n@JpS&2%cAOgbGxQWj{=A(GXvzP zZd#P3FiD2p(D|rauY1gX-u=4evHGLmrci>&w360=$Lpc&$?uF~SRfD7oTmkOpq%5C zE4Dt7PrtO?qyIsc%Cj^=-8VWk3Rpe1i$l#r$o&S1U1en52?shn;V2%7Q)QUX))Du< z8c=dq(d1_(;c9;|u0S+iPO_Wp65T~F7z`25@aF&m$*@a;XmuUI)TJxRV&%;SKQ9u^d#W(9V|vqlYV(BKGGb>X%BOSfVSYYSPLmBja&PY%4w585s3mo9Nw&=-uETw-Ka4h2^e zzDa-Q@fjbSdY+zV(yIXOvAdpC*`ql^v1uRO)WsvKW6VVVhCHmBr83Ya_9XoF^&OF7 z5Hwi`c^Ytsf?6+M=mZEztJy$>VgiThnI*_261D4F2r}&n-eg>Px1s!Y8LJ9?iZGa| zGl~Dhf{8@HW3AHNElu~emnJ>Wi7ANExS&~Ux83q5a&e}tYL`;7%?BnWp_mb)+&s|w zJx&+VUih2*h9Yxw8QS*cGTl^n8@$|8+naRhrcuM-N=5Q$7>kiye=$blD2z}{P?vPm z!aR{7G^K94b&AkQmq9)ziAK?k$3oqmza~#-!gO$eT5TpK$nM#w+$naw zOiNp1XXkD{0Ec=k@I%|=^`4k0?fC%)9lAJ_FI`}|aAtX=T&>4)$e2|J8mhzHT53sWfWB2}CV- zUtSf^32qA>Ipxi~$#Sr2uzN^Z50aQj9vj>$< zTICu*of?B4Rs7Vo)NY+d(KVZUm9RFZWGnL4o94^W8oi%v$cYZt7xYpU(m(f?`VdmR zJ$q?6Lpw$85$M{3x(_{c_C%Hv1#t1*w$VL)nV_ahjf(f3$Ve8nkdTmQf<}VCC5`P^ zU%Q;QZNWXXmxRXeMerDXc3N3y50e5JNW+lRnFPQ$-?b*KSsJPk1D8S?>o{yo%cg2( z7Jwv1O1S6m6Aj!-H8#kf05Z*{Iq6>(qq{GvcVC{P;0dAPcZis|z~!~tTP=|Q&e7j3 zpU=vwP}dK1R^H>}j1G9*m|jp_>u!CmK_ugH3PUO&n;eOWKTIH?(gV-0iPJWx1KN9c5~?s#ze9rbEW z>{IM9s^V7iR3?g*jFm#e_Pb1n_xt(D_X`uTk~*F=MJZVeDHLS5=4{$vGD1pj1WJ$MD7xvzfc_7kPUQYp?o8TN_D%C2; zUw9kaa`$v{C7W*X)evNwMM1JwSW8E;1N%nWFUswl#d4N5Du9GVRT-iigCOKFwPpW4 z&QYDgeKLiU3o#yJcbdECqS42-}sNR~iSAAuI`wYx;++BX0E#Dm{BA z4_g?#V^c)1D2QzPN$pRhc91A!z4=~#>#L4&s2@%?%T*l;S!|LVTCdmFKk2OGpHM}F zJv}K}LW|e<{^4E82T{YLPAy!)5%j(qd`3o3(Sl{~Z=;-IorG}@RwvN6*JjeQe9O^J z8klX94mtKe)6x$p_sSMSb}TM;aw2z>6CUS$Z>`uXzr|KM{)TTn8(rfPr2bT> zB#3{x>?VQmJ|<`3b7{tAiS6i&oWO1Lkljqo#QsVQe+knbHXaeJh{s>xoZ^4^;UmY1 z6MA|drfwy8Sw&bHsA%E$9&DF3si`w<&9zce=wE)&n-0&JE2#yx?mK2 zNrI~UnMpfU!KhGd;S9AC-*zWbo9M7?;j^<#fQqHTX*_wMDL?C>(~LcX{fntl;tsby z=T@xcXRK!y>*pX83f6Xc%jgj{Yqh9*d*$;XIZFkt{#ushJS|P3cy6RM zJF*oqULDoB^!Lp?4ex~S4w@Hgj|aJzaaok@B<+`Ms%h$M`gc~oS7S)Rw8t4}judQ} zxYiroxz*#rF>JH!(Bg-7d)&Wwg17H6#_=Gw)i27Tgz0n1?tIxDF74iJ7sb~uv!x>! zgby0Nm(p~GZ#m74aJG%o6b?O?-ruh}vp~?49xk&a*xuSJCrlT3kR@L`>!8J!H9T-e zGtA-hTAwFwIV)Sy>?gk66uODGF9@FrJc;^Ww)oeklQ7fL-uRGPR;Nr9*f(O=`*^Wv z!@_QMs+>Fv!8KU4-k3h$Z8*1GFTB<6xNrqGlbcqi*XTtKWZ)1l&ZCLZr;wa^0;rm~nYEo!h(X55@)u zN1Y!G&#hs5r?x9qSea6~?9WhlB9=t(JuBU|G&nihboojA$XMTyr0~2-Odo$}`SY|z zLgQC$`$0O(sVXF<$mCgoY;vL;d(+5;2xc1Ie#ePACE3AV9T)AgS^B-ndbY~V@erLs z+jy#fQ^?;cy2^+B1|P4})YF7iT-skKP*Oca3vAu>7)+*Ry^qjy!x)HZRvf~482iE6!-LtqpwoDV&J+1L(~)L97EP6v*3-NbX1e_G2No3r z4d#XY3#?K zzOdIcxIH*(;ipu&ImL%Ao#Ba*^Bi6-T_%%oTcpeBe;RcwMw)+a*q70DIe~9x$R-M_ z&UU%k()C`&BNKC{R@;m%r`0&0@ta6NwiXL@Q*M<-D%YPUA{MmQ0`EzLs%vYAIF1z0 z)GU{-Xqv5ajvB?<*}JZFbF-UcUJH3}UOY$$U%9W$8k%w!51QQb@4!sn`|0 zlI4Mg&wiLKHq0IN!7^~~GfX8}as|27&x>ite7_uQsE z`Ov)??q2nVu(Y~^?&P&yTl1yZx`}*)_(7=#^iZ+QM8)26>x#6f3cgFI112qxw*+I` zdXiA!ZKc|3gFNDSM&XE(xbBxBjIC}OK4sb7auGV##UMW5mOpT}Q@w1K&jJE&=#=s4 zGN8WUQv2KWYV%e)pt{v`mo3wV!*->~QoU@d&lycgzwO!8XkXE3U(<|=uQcmHkGSam z2or@M4ezZa8+r@K8K_OvNND6h)l zyang#kA2s91F?`-67J){Yx!C3bVxrxwr$_}Z}t3Fmg)Y0r5=BIs*qi0rj^kU*&Ut0 z@lKZ!E;tj#jCXU}xkR9tqoOE>f2Ww-QC=RN9n&S$2iF?5CICI{gD->&<%V zPIp#H8AiFod6TvGl5=#*IDDaTIss(5FVk-96f|Do1wZ+7A|{&O?_^aj4CfZ?>(vpUZCvM=m>| z4)%B+=>@}J*9XRj$|5OV*=4sIwJg$Te+0@*(~CUO+_^i6vyg{F*37u|H+tB zVu|5+o1Are;5Z|Ea64M+bf?^I@CZvN)%U{AwTg-nMy?S_mn{4$Nq!W<(FboI6UaYa z(~2Y>s+)l&zSL~ogFW;;N@yIUz}ZH zOJ!^(hEpYMJQ+jJPgr-Q{X_RZaH|kKT_`5lY-TH#XufdFBlp20Re5E`YHDHlRbQyz z*Wp)f667LCy`LzUg~8|%5iLg?v>6xPI}n9#r$FC@QP15LwEufRuw^nBdIqPm%=N_ zfB)})-rm^e=w)k+6BTo6j5Rhgy|Qm;EWvLmLzu|B6MFA+Ym6mZ zs`}ULI*I|f)S4I1kllFm(l1QhZtme(qr>e#27u5@Pq4f#4`EA^S$e~hBpZ<5qbc|_ zqf}f*%e--$;I1VL3501>lUK%K4!37k+a8krFjTRalh4--SQM#<2}Gk%pRG9LGSok& zdU2HgAdYX!P~>n#3li47(BPZB2e+$v$zL~bT#L>>uocy#kO}h0KF6S^^6H~kwuXs` zUCNG!Alz$zWX1lnn^Wd-gLM1Gqar@)$VxttslE?lRUxzBHggtms?#**>}k038YqG? zWnXtU>4>O$M{t5y_9a!3zD2zt7$M}vi)flZmn0(*`eJPI!{xE4;PvPT6=I9k5BY=E z--GpK03`9;IH4!|>8B;Bck@TAs;7i5Fh>6|21hZm|5I&uk^9Jg#wRNUp{`|(o65um z#vO0Y;+4P1u8~w0Ejr5^a?Qa`C6p>#3~rZfy}#0(3G2F7l{YjyEkF2`|Fjl|4OSJ| zN8wmD3KqP0MPJX6<@(Ma-bFKkrOQD9Pg}x?DrOlpAIAUL z_W{igW#Ql!AR!bi<2yAY@D{n^HXx(o-4w#S@)(%#JxZ*1BGJIVe>rx#ni9Q0@hH z&_&e{>ARwo>}6&6>2s+G$E#d-0AKg0A5~w2fi4yz_4lkdofk$|0;IO&-A7-s6g7@< zVPktgNeOLB!mm2tWoB8zGD<`gy871S{h!x@=N~Up&}cklyD;n&bkdC?z@W8+nVB|2 zHJ$TGBMHEF)Pq!7%6Fthp(t+?IFRc9`dK)qtPbAYpq+Vn0DK7dr?|K)C-kh9I)@Yio=WNOsSw>d*$=CTcjQDw`XZTF zm}!+o)z|lVfHu2n=-a9`j7P$GS?oqKu(zyb`&)kacuEWZ5_u?Ps?zblwM?FHb1QG6 z@r`F8eJILTW#HQ$BE%pE;|4qf!7V)nPfLKgvvO*VQ>M{Ueghl7{njKBPI$Q<9#+xc zXqG;wc(f76)3+jl)E}SFYaooAKK*Cx{>q0BBz3+tE*)%AR|0<3slHFVDnx;=@$vi+ zU-IBQpC%(M?Xv(k;G=eNNLOfY7+w7bN}k^_-B88xHWw0o1^09vEiEnONmb7e;ISlI`#GfbXA zs6gtCVa99v)Wi1Y2SM~82NcWNi@IAD;E8DuKc2)GS@EEttB${ z^zg{Wnbin`!b;!v%{79m=(z1lkWhw$SJ*a^_Je_ok1F6Rd%lwc*DTI{WC@vGR@llS z>Z>mj;T&_@UFZ#0alA9FX>78X@g%+jO-9%b!B{@_fGVV|sZ7AjO8cualLABK2>_27 z;&?w1)Ej^cb|%TZ!W(+q}r5Cz`cjvoeBv`+|4>-=MzoQ0g{Er^KZyfro{;8625 z&~qO7yI_Sng5guDz?+#+gJ9Cf$Ka!-y`*5iLoQH?SQ=O#Zg3uaxmbFU89>3#*B}-c zDicUb1hJ)5Xw6nSt*igLlQ5RzA!2kI4^Qi+?k_nUNzOvrzvXgn9NcRE^4vNCNg)7| zo$T2lT;50l*3|>RPw+8pkk3j4HuNvS1x1m*%VFK zxD1qTsD3CNpeomiv2YqH-amutU!Db|5OkXYwNCFgm5Jc0C60e08xNk-IH9K@A8rq4 z5Zh+}8jlK~)B!$FlsgdhT*E)G$e{I?qCe$adS*H9X5e=0h$912-b`=%w}}Ws&ex z+}zyPXm31wD$dgdO!IvoNYLWF7h!zX39J|fpyJK`q+rsT_J-3zBuGOlZgAd7H?(es zHNGd{t((Yn5PJYyQ&9rf%GO(X&m}8XjY$wHr+0rRm}Kl+8+I7B-=VX%O~e8Q<9LI! zq@%t@5+;0TQ#dkDH2`ZBH6R3u_Ei?;8)v4yF|*Ub4D-^)FM-~Z&YuTMK+%ii;^Im` zI=_+j4LAoilBaufrOC<3Je7IXE-IpYT%I87bANjd2?J2vN0DWnbuIv?bGNf-Nr*yk zx`1^*!ttIG(A9*)<4i~|@0J(gR}IXvfE-jT2da-Lq}G7kFNezZffhJTO!l>c05L zZv+s;aR5ZM#w#xfLV+ZD@lFXGy_Sdjx?rzlAcv5fDE6nNf(P4}Ar~+SK*lVd{K!sJ zMftFRjJLmaz$vzI9Q20U{|)BBu@b^}EdDLU`3D5dSs-2R0$DX!8cWC3>DfKn+98pmeIpr^Q%Kr0>#(;fH?~`maAcRsn%;`X}Xm9v}^X z-w2CHO%NW*m=y#3eYTzhn^4=vLi=N*3N%0*>Er$4#f$2D_wMb;-FcX)8ef2{aB+6l z{J+kp)@jgcz2>qgK`oc{dR7F~6{8!5wSo7NRXB=Av~b4oX6<;-)md$|-gE};Xq7Gr zF6`p?j3phWIuSa!;`=LunixPEu4MdjysnWT*84f)xDfr|@EZv9x3l5n@E2dbB*^Ni z4$Fj-xe?v2JZ82d!eX;Tg|WM{LiO=m_wQmAkf|E0z_$ELGO6T$*% z{XIq#9KBt_y5_=WQleXeRQ52Dgvbou*>mS=?`@FR|BS|67e!kXh{{CW%C|v$#lD#8 zk$sN=O6DjwK;q3U+Z*#@L%;f5lEe2Oml)u{qV4+X_bPhMnOL9o`EQpxd;J4=T@@!x z1}A7A8j6I!G6PjC23edju0Cb4KAuFgen5PHTbl%4f)z)(%WB+?-&GeI?PnGJyJ@)p zRTu%uVLEFj0S`?!Tn>Ymmnp)drE0r2ZYoGAb8;z`hb~sYAhUpK<34pbc)6!)IC>lR zPvb=iE}s80zIfV`fcu~V>7~=wzJE&OT=B%&`SQsm`ev^d?=5x97<)kwws?I5=P(#G`Lcb+eW4xlxzB34gc(;$ zKm$NRK5~res7CmYB6_*Z4o!FPpI)gzk2$W!N!&q8T)YkQ72$tjg?yGeO9+lreJ<45 zt9|q76O|X8NwV548a{POkT$BkR{b&rvVo38NE@M`LwY{dCQADQ&Pzg}JdSC0^$&-Y z#EKz$&Kzzq@&O)&^g&#E7n3%pc|KFl7O1jp+V#Hf#(6Qpf0|`36MTyu=ZA7#MUf)n zMaJi80A4ZD-C16%zE&l0WeMPHOrY8^sDJO{{hiR~8Ct=ICD;rpP~~{M;a(NE2!2di zD%`#Uxk=nOfY@E0n~V7PVd~%psrS^bX$*{Efp65n{yTkFiucOC{|GK*2Yb!%4tyD? za7iPKm5D;nW;nq(KBz)}9x}^EphE8o{qFyynhz!EGjk@tL&>JO2kCIO0bb^UCXgc( zLI^Yb&dWF>a8T257C>l&_1U zcU;X1PeumnOe6&V+1)cF88%d=0hnHB0^0DxEEZnVML~*0#g)U1$8Y%QELlpqnon$|L*bD=xkP)SVzDXR^8zKVTuJdZ0 zuuASJd5xBK@omFD*%iOxbGmwvL zHh$In$2`Yan<0p{2_mfibGr)IL6rPMb4O-8GvW$(W5kYq4Ozl(7q#gtQ`fkxB3i^n zH3Nt2KuW4AkVt!w|4V)=Y6JV8KoLa2Q&x_Se14hC5X2Y*xHxKzKe0>HK1Kqkk0kK= z)vH&a6fVdTK=6P!1hx|-sf44~69#bkRIdPxOQaQf?1c}q1cq3F zYn0%RT%xxLBPLZf-N{66K&~Ey%?#I{6bnn^=oMFJ!j_NmWXL?Po`jmH+g&ODQF$S) zQX_ntRhJHacVyU#u$;eI#rTteWQHGGx?dh{ z*18_RmV!kU?hu^|m)enbbadi&Yv92F>%z=4m08>>Cegs*m>h~!sZ+gLp%y?nn5IZcoe)5RjT+~VT z{o%x%u~!s5=+3&^qX%-XKkw0`((FyW6!v2+a1?i|EU*UZCGH$FrtNU9jv?wt%ZP4~ z_OmZ*b@`n;XfTX8@kw_-4e@NF*=78aTaa7f7|p?Ia*<0*^nQt5j|P8iX}!%nIw1fY zI>t`Oez?}^UN>NtMa6IEhI+d4Ao`A-RHvQE%_l_EOC zMwA~!7xLq+oI97ZT+%t=1x^pml5}7CpndaZ=~jm{yWif7Ad#ipo_kMg`P#Xxpcw%d zwCiD_%L-{_`SxLVb+xixQBzB6ny%Jh(Ne+SM9DKsCI^|ZiCl+K!H$*nD==GzeMbx0 zvn1Nzb^c4Uoc%7il~(g6sFS^qQ1yHK#gJ&OJ1{g&jg!DNtWP)2PF^DajSenAoHLy^eR(G9fR6u8xh4fVJ z;7Xn};BJ@itc5LO-fuUW=w|3{6km`JY(=_Cxm8+E*woWdWR19w*4?vSv}WvbpODts z{M2W$XFhSWo>HxHy0@~7wuE;+P?=tRFWP1+!s+_Xy4o_i`}PEqI(OeRvuZVP@Zd7d zew)O^;OLG8!VDjCus*{=b3vKfzGe^5Lk~Y9n!D8wt|ZAusV=ne!l7ha+>!U;BlT`# znBNoLUU5Y0Npuk|hTs_2Q*X?vgHzDw>s1SKZ09=oX5SXpvm=T#n5{NZ0Gi}n(L*-l z5rh8NR!HHw4}4=a!Si9mR>R#n%Xb#K(1)%lo*c9gd-_VU$677BvtGqvtx_c=QqW$b zNUd_VJ%D70YOi0uenA%9c$XwyHRX8spyy=NHYBE~Bj2;FQtZ1nWMb6_ch$c(HhfdIxZ0hv~AihsHFTn5hppmMr~(5R)%Z3+$!)Z~7b# zdwbCCn=dvtp2#iW!ecHK3zWzcVFSRy2R=rlgydxTFg>d4QnQXTpn!c9_2GW$Z8FJZ zERAF>?7OOi9#E?{)6I3y$f>)$le5|TNz8A2u3W1J9V9ewVEz3YZWo&ThB}eU;*OrER=fUXal4CZF`S}Ej6eV@$MX)Q~pSIlp_f1{L^|n6MAa*Da57Z z;dNLRTnovHr8Yv^$O+<^?UORy7_yHtANS94r+?ZK9f2&FPZO?}KAa#$H62*hCUSz= z+Fir|-_)7pp3y^enp&BF-0hv3{e9k+{il>pUF|#eX~bn)^P3|Tsn&o;ZA2Q-+7y&r z*&@6GbT=$!gSlK5Iie*T8%pKYk}JWCZHCKUy7+9v`8lghwYYAR+TNzvNrTZc*1REjL{7!aS z?}20ozhals2}h3(4bBst06gSNYLC`Rw~>b!$w%(h=K_)qa+57CAFhn+imR!qMLYHM zueArcl_4rrt<{C!nkV=aZ}r=SX@B{;GPtum-0$*N;iyUPKVFkgz+BeVI~n-%Iz?2d>a`co)taW8Z)I7}B#q0AdxfCTejMTCAi2(_-?K|yg*_|UApt;;TOeS1eWUcfSToP&Q4$_XjRt2HEpvZn9 z-`0eu!esY75`G+JxmwceRXtgxjOJWy0df#anN}fU}RCLgxMDpK9t-<2bE1& zZFa;%(5b(a-BP24tH`){iy6wS(%0GlMb_+Y4FGp>p&{7>#37)W}8T?qqwDoKQ)=)mYW+ z+(H(O`@%Qe+WMf~S%c8lvbo(#0fs!S?8l-N_h;wRvveve3)b515!RBOY|U7z^R#rb ziMF2B*g=d=m#;18YCjYXOS@Efu`lTWQA2|`EOYFUfpmSK#%WbbP3IaN`-{|33(9Qwwh{JtKhpO-Vs)5UdZb#RXAD(8#Sw%6xme zE8yNLII(CM`_6I?mJSlm2a&y))IcL(^H~b*qf>i6!uw1*Ta9iDxQZzuLh|kdo%$_9 zx1DNrSSO|G0Z^0|?Z5XkXy)maDP2R|Z1Il|)G5fDB^b1%X&tl)5}@3lYh`=wK4Pba z(B55;5atuMAF|#9c)UHu?oMTS=jX>H1CyC`Y4$+bTO&nt+p{_LmU#+g>!j>Wq35+( z5qpDv6OF=$#R!a#@V2MpOq^va+7Ay8J$z_q-9Hu9J1Bo*Dpbqjd_4`ie>W%Z-QAkH zCyHi_@|dcIwe>KJi+L|>+{Pga`bkt=@1U#?M;~}(Dz5eCuXdJfDk)Y#7IgSz!ep+6 zCe~dCQ|^&ZA>`h{WL@RU*lmBa<4K53{tKV-J?*svG?mU^rh77)!lHbM0F`*J(>KOp zfC;gRS?S3#8QPX5!>#}kg^9ps_>v&Hk52Azzyw8<``ROW&CNG}YQ(-NAZNfdorn4j zZ~4^6?Wrupu1O%SY&3UBLR9+?{)?Blc-PO0V(^Q{co=%a@nGFOrVED;-8YjXyH=J+?= zH`32QabeWwz4pU>%ZeeBrJog9u42)y9W$n-u*DhA)7PLH!2_uB^r9WK?$d^k{A0i! zLEE|3A(v%ea7~-=t6cPMbL=oxXR>^2IuvkuH#Y`6rQIiSVUZ7qAAY+Z6v z(;Y}-wTUcH+mxJ;S>Xe7THp5Hqj=SYdvq7!M46o{JN9rC<7p}eHrFtm5pJ|Dp)3Qeuquk&#{~ACcvH$)EqAVE1E=v2`f6vQD zafGpD)`7!+^xHp)t(Ejv-iD^IT+33^)`BWa1zhCEKNL_`euFIy2FXJu*nqFZ@j;f< z2UDO(eMtZAOx^X>VC4RdkuBuPj}_C5`vNnIHw{{6mO1Ns9?8K~yKUcqHtD^mQaGSg ztc-hZW0g*bXeP!T(fN9GwWPN$r{1+>E*x$uN#wCUAi3}x3aNlsyl>aW0q(00s-+Km z6dB|=(VuIuQjUJ3wo<7p6IGyzi^0DK#}&Y?V}=koQ`MmH0~25?EnsKNqnPp3R2uEl zncIpoj7yRgQ#0l>Kf|$#uKPhGhx3{&u^~lcfNd&bN`kep3)f0Jq7b3I+8p#f)378q#1#QTNpYZfcJeh}3 zDZA$HQg-SGj->?QZc`q1R++&$M|t^H?8F;rBTxbBRa<}yDFGk;Do8;9AD$R_834w( z=`m$mU#LK}G5>9`+mRIXfdm9WRnoc}j$i_cgkPXJ3Xu1UVx=V|pp%4p%dJYJ z`USrVkbHvTbn+Zc1h!W{g$uju-OyAFnD2E( zBTB+%UCJ}$tPS-&A(`)s#(vDLO?fl5pCJvl7 za^F;m?K~_sYJMo$_el~ED`iet(Wm39fL_=AG!SQ$^0~}B?)~uj3GOpktfRUD^~q6h zv(r0B1*1NbgYN9o>m2z6Q8J)XyB=3Krsjvaozw6+Tqj+mZ%f)O37{1F1 zELp63)%Vugs{IVCT2Ah)jWpkSDAspP`9SVhLo_!1*JR4SGXCEqdEr{Oo?L%o{O9tU z2N6S!)8^@;9LmbM-EctCJ+0fcYpc@%;6uulK5bG@h%fr8p`>Qk++m1fBe%nV)H{8> zs|YS>$x9Y#e7lU~6;{-18!(geYrdtCC5U@RRDvFKmwX|vm;&#}9o|qjZD#NfGd&zI zHF{9)CcE*7(qVt%ARL@qConNbkul3vFAb%*GPRzdtqPj<^fHR3ih$6v2na3Ws!@RL zGK6A9&Vz|`E-Wo883j(%g_TlPK@K8PD^>Bd@HGR{hYKv z*}jJf96`iC_n)DFENCdeW+k|^6tpTpPkBuPnsZ zDCS4OBWh+CK>nlQ6K1Tc=L}y!$MTF0?(WmDPKBsKX4+>Pr?~`RiYOeX0A=!GHhx(i z2o%pX5Xa-l{g_QUYF`r%W_ek0zjK3mQsiSb2#hl$IDXMW&44SkFQgYY3g~ZxBa(WE zxExVjX+ZKYN3MJd7x##_{Jct8F}(7{YCFcZ>|bbxp%<4{ar;+cbVlR_2t-a8Q1aq< zPrSz5F9D+X#7OIURsNj|+!KF){r?FCF64f)OoYB~`+3~@v0JuClZ7F0SDc7srfV>zd@`&+#E^aC=R2p_}eDmFG?Lnkj^zD!I+cI#&RWm7% zTY#Q&7a$669dHMrzPt+v+D0OI(xVKFP9<^1aNP3iEq1KJwn?%6%JKkFDr#F6nWfffHVM4FV7 z3GrS}g0tl=@LID-uH#-hwft9%j`oD!K+hwFdrkePs984W$-(o%*Z(IRhG~BpbXe$oJ4O_w)#Jjz_Sw2@pJEhYl7kU+Yk?X z`V#o-_r~Ye%>K{l{-E9ms>_bJdcOt{Q~?X_WD*=dqzwxQ>`YrO+y4oR8AuBdp#HbU z!T@a;ns*rZJ&6}o9N=s9!o%w7k58j;fwwrcuTI3jGMyWRAP1A&=mT_m0m}c6z4ri$ zD($*PM-)Mkph%8JRI-wksEja*iisdmK*>=gXGtnaMFBxTML@}ju0d~^85d#moPTX&}Fo$0sAw!6>s?6mgUYk%YQ{fw1IC|m<08Tcy94_X7N zulMHYlH&{lQ56a{9aBQanigRk6FCRu%uf3&lj*?AYrQVSvNzcI9KSV})*!NvSp?zm0k%{qIC8{R- zV?9kxS;YGi1s6BK@k)g;YY!Xr?L~2mLkHH}VlvRUfg|;Ef$h)r{TY~!+^)7k?`VBE z8qr3M7qOP}AxO?12Na{9vd%QXr$k@f%6Q=fWc|RD%eF8r@FJ$85JrYv>O&e(sy(kv zm^G<-uF{nau?+6H<+-rVi)G!QmjKr&`)ytdTxEW%*k%?wJ6ffDvJ43TH zf#q3KxD|WF>oMh?c>7_cs$DdOn20#AHW))38EXxd&t*dXBo&IKH zzxxd#966)ZYl(@4!4+Y>h^2E^|;LAen$KDYv-tfG*L@d|3ic;FIu@q-EA<|BZBlX!8I1sz}*{8>tWRPaFZ z4BjDNoQn(@*}fJKj+DT&KVf3C6e{MvQj1FJ>Oc5DCkPrH?bFlGfEDZt@d9=R{i7tx z!i1arl2=iiKSPvWJ82ReXk|n^E?j=?A=Yc|*6ht6kq`dyGBpe&ugOL1P2x89)7IcH zmAQ(h7eQyN>1E2emtRLK)!*CXX?K=kd8koJbB;mGyN$?5Ez6eQuekYUn6z+dLxXb1 z58Uff&b@X@l=I3PkpbxpZe3N8Alm`w(aO!EsZDE0)S<)NvotU?$^u{+OpmUkJE6Vl z^%)@mMfgxh2p|Jiew$?9`#p_FA4#QOEtoxrEyTM?aJmlN-$K${kwdwQA77FEX=q^l zQ>>6)rS>;2WBwxu*v=wI8+*uYZP?V#GArQq)5%hQ#!4c2z%)tiMAG3ivAizu03%`CWT?aHIKuNda35(Hp$aK{jj^Pffr9u%PFf!sm| zAqVF;JPzgGe_Y%@j`Tk+?vE=7MDqWeTwIWgLUmZ7zaed_Oo+&RQx>e*tNe{vf;hzxY>m^e95Ca z_gb41*%3EmMWjhgnlrFNi^UkGd^buJ_u?e>_q%`?ar-nD} z4@II^?U{c$+Tn%WxHiZ0ro}jk`;5o*R;(hSX(}cr=G5txa`&9ASe}7F%RjF8-=CJ( zcBH%XkX-f9qn9wQ?DPiuCH+Y%sBd_CZH<_3PlkOE^Au8LzPmvnwDZ8Z5e=_J#y5UN z)Zf1QeIPv>evMp<$NzgvZf#nJAL@1!{#NMTte0PN3kf5K5PFVt2M~;p(w;_;4__E= zwP{(WdG#F79?5HdYi&#P5k0x5waDKMVhqp!8}RtyBmVZ{|Gxts&upU#2V-3Ni}2B0 zB!WkH<#;YhdO-HDY)+?r=NdgttUY#cV(N)h@kE!d@t40F;g1LD)rX{)KT6uXS0C;vIHesgddn}#q{jJ z0GDK%vqjw?2sl~2>iSlZs0_Ty-H5YWVn=Y~U~&(>Rxwaa{G1FER<3;*83x_uC2{?m zLU~fHwJ`-IPwWPUGZzP>t0l+POUZhV4R$SjR1B`s)HaIF?(;KCKbS{>KF1V-kcQ8b ztt)!m6?M?)HmuxmCluvPV{Letk4& z7DMP7tjw)jRdDN7a3VX%P5kUH8|jcycg(CRsC5fvbyLgXA5ZbGFPNj&TC}3HTR{H6 z|I$IdZ{0B+jffoZlbj4%22s!NC#%p2T+%Ff!wTYtW;M|+$}69*7(L+>bS#~jwzD*& zXquVtA-El=lOz#4p5+l60}Mig#MAJtViT~PJ#V(Lx}S?gG+KRP?_jkPwj!lO9JMtZ zf3V~*B1`@-TiLk^*I{O)azng}Ib7TQWKGvq?>0Arc0Z!o(|uT(6nd++cElTH$8UOfVd56|CRW?eTtJctsHf z=esCi9<8>`zETcyni=@I(hVA0eJ0ACe8AB12VLN~8M8)RTYNqbDH^jfUSU(0-}^o! z(Q~q#KDa)2c}Xm!r&hc^*UTe6v+L$e?V{DGDp&>1A!&}WsDX7H_IIq%V#rfD7zn~X zRJ**%hrI5b{3V#d(9?1rzMi?pMxTsvC_W-Hxw>a$If#vlQEErA=C)6?#rWH0eUoK9 z+Aw9nz1m>CqjZ8U48-5C_Sx}bQ4Zm~3P)sJnY zsP7wR4i97xUDY*Dh$i?n4{#Ar72N^Tdvwk?ynaLaJyzW@WPnB1>qG4tXvNDax?J zMhcT-mZ(BPj{0lUh;l;Le<8#2h;i{2fnLDj$hf9L3T4?i_s<1pX@M+93_lSRt+X6* zg1f2GX3C;0b4)HBARq$v5huJ~b7m5LSJr>XKYr?$_5s7E!6W!O2|I zjjVVj(utzwX#dKCx>9!eSey3-09o3glt$AUN4yNyT@J}86twW&_c6^rI7o@w!>2@LUY z{i5v`SC1+qx`{J(%|U2`V~X-tDNuq(mEcY5q(~gQMK!%q(<)nqNKC9wm(#Sbn$9gD zd6VjRXD_Nhk~z4^W!!8&1hnxh2pCdX-U7@Xxc4_Ga8-1jQVoP97B-m(&mX{D)p&l5wpY)FV#Hjq;UBpdk^^Kg6eY!1ck8%`Q`ny_g!*c&voWZGTs+D>=!ntA+j zn++Bmv|Gnzq*IL)n#doWY`J^eiBNp^H`kIiNkVl>TB?(5PP{!Q$tc>+Qg&0|E?l#2$)T=Mor;#1-W&Ar0|vpz4tGY* zhLkDRlN=Z3(yfM~X>6Vt#m*(r<(jU{JETzorQLV3WJczmM{jaiAO&f$gh0JKW*ckW zZP)5wMkD*$gG80ZEwof|x1LZ!DzY9n5*YPh@=zk4)jl>KT_UlYT!kWK8#s~uoUTX4O->xwIU zwl;2yKUZ_Fn24W^Zpp}9V&T^fdc=tUHFp^gv8&tIVwYx(<~oDKpSa9xmg!0@4i%M) z$#_o+`*Vin(vw)bIPIL|7SbGwq}grC>1USHN(RiX6xNURGF2_lnU!?mXBNU^b_(P} zFNz?$v^uAk7;V4Rq02@wua~&8JgDqZ+iQ4yaAv70x5wGc@yjm%`P@S*{zr7#VrHJ` zFSi^YbSyKg$)Ku@N4dXf5WjZe7EWuLKx1SBN&s$BS!Tof3nq;rYun_@m z`m>g2OGhkOfxecZNtKmcXhW@IH`E{7MmwQ@z2~S=asF(&7N&$_KEKzMRN_#y(q%n+ zL3xC$idM~SxElAkUM9yQE%(vzwCAMN>e44`qSD>Lk%|C{vP?(fipR1&qtv;okj2r4 zkj0tFGA_~q60yi2mJQ}&nr7;&H)6R+pL?;vk`oagOx3n{lBet_#SNa~F`vl~m(JB; z0zBL%X2SgkNzDOERlQ4J!VGLj5;$XI%I>ndgkXHwn;Bf zS`5ZK@{nbwtK?V4SYN-8-ClT9#$k#`B~U{f?(gZnxf&B7u0?#Wgx$hp$xiC$axivW zwW;#+m-^yK6eq0uHjY+H{x$3PV{+==Ic^m>=9vIPM3A%l)3#g@K$t??DlGDDO&+ax zbI+R&(5>;}T)Ic$udJ=*bGb}kQ)X$x0mH=U(1u$HNLwHpm6a^7nvf*i7b*nZx6?Qd zd9BzoU8^6Gl<17Kg)7!T>v44`-!Hag_6V+`3d4u7inkuSQZFOmI;Euh0n@7`mq&76 z_N|tgdF9?aRd$?$tFbp`7Exb!95@owLt2gMC8|n{yjLFPB9LKcWG0_Yj$l>_R~Pf_ z!!Y{Htu7o38UDVY@onrb0ncb$rC%)BJ3S=YOveti8l{3;iIoa84vi(6fuU|an(h%J)Yo>Cb#PefA}H*URZwN&?hXvDW&e^3J-WydIUhr1H1^5gM|6 z_d~|jdZfR;ukx|2b5~k;uk67}2sg*dN zE#VkZOV4)>O~QJVj5Xml&>E+TXra!tdll}w2h=V*e(teTd)?KJ=WU-I(6rpLEHKe3 zE1X5>!M#?JWtnd?2wBbV@edFvka)5%9rVbGDTZ|4`r~nmfznSUsVhtMvo3SV=cojo zrYemVDwbv4OHbmyChlaoF||Mh_d{!q{F07t}hi_??)`Jh~1X%_|;@3GetyvMbPaJoBLZM z^)wXuN}7XamEuoWXpT+l={Y+eBhKeysF8_KY*eqa zJ*QL!l>vFTT8ZoR)r=ZPB9W=+fa}C`b$=CIZdTFAhw8Gfu9fNj1!Ae<_4;>(i($Qk zRTNS~$8n8HGWR`VUKr6;zO{&5t{v zo=nHS=V7QRrcKVD>JyNp>F9ZMPjmt|Q;-9hQKt*wm$8!igqYb!p6&Qn24UDs{NrdeQ^NdvQjxB&Q-i5A7RM zCvm%Ik8+cWL^t4COIFiyf-y_sY#P*8j0=?{u97Hx!((0bdsacKUe@pc-?zZ(zZLW{9?$YB)3~8lzxz`Q6xIYXXC3}pa5l6T)?8pvo#7gYgfNQC(GQ@MH zaMwI<`xqJ9K9=42fl0i_(EuiH!~J>Fh*(CCY0JWHN!Otwg73<_%jf;AZga_M{)y84 z?NLv-q^4p#oQ)j&=*29Xj+#IYz;6^glQS8XRX0pZVfLf(OPmS_)v-U}`1K$UA-x^W zF{jg#N?D3HgoLk`y*k9nq^5J!g3_y)fnB3nF}CIlmEt5d((#`vR7(g zD6VWv6-J__&Qj+5$_KTyI&4;!KQ^iD<3_GYPxHcBdTgxhs*U`(04=59LYpGCl!B`b zvtRXX@xp?BR2}m<_hrqpe&-1r!6E<1ddc~GoL#!KG=>ml<*ek`DO+-c#Gczt{8Yg- z-xp6#OVULsSLj{&{f)$^U0CpTI${zPH;h5 zmX=vDWcc(6D(FK=*Y+IR+yPSkGx1xoQ;(_kpz_}-&%B05meDlqgJdunNfjVPe`r0G z{~UhA-ze8I#!25P#~-4k_cleNGSYEp1Z{n>jZ)e>1A7vL=ufOCC6%)aD6Kwc=C6|L z8H=1Sb2mJpV2Q3E1M2;I!D?!fDYCjgFvc;|P5@OSmra8zi(!3fn`}OKTcAP?bEd+jff>-lz|BWRQ<+Q>Jn8HZ00Gt>+`2OED;)!yW;)@-`C*=H)TUH{GVEg-S9 zeLb0=&ImZA6Z^Hu;fFueLt)`T=2@u)l$rthPi!$;u`xcVw;whmq;7}nlG8aZBr1A6 zpds~fAb)rAEo-G?C1`C}upjkQ>U)+6CE?JSG0YXz*s@G{Vl9;#3Q)t#Dh-1<-K|H zDJb4&ihI>0w-Xd{i0t+CwY5Q|^)W@eV3N|QPXE9{Cz};sZR#Npu2g)$l5W;>oY+QHJVO5)3tfl) z8?4ji<9P=sYB88SfFh1BG6^$xT^}6xy%lx%D4H;)DbsB|9Dln@as=8PJa!_R_!&Ou3vTDtqaIa(}OR3VR@fkBw%7`mHh4eGzBE%FjZ4J^<;k> z+V|7kdAoiuv6;HU1K(>#(7wFg@&fNlI26cv#l|U{RE|fu2TwCn@KUl}A;r%eIpc>nNIeRqpFgg_m<<^b~i*ds@trU)w$Qca3|flcU{16-akTGB~}uF zzD!pwjYe9k8!Rj*^mB16?g!6bNQ! za`gpgSm-KsLD|Nb6V58@fEf6-Ln9_d!14}Whvj`>4{WWAdoMajsp%?IK!``2#atnHCceGt~Vs_no)?uBeb%U-Td0~2qq z=1P*MP%tROhlhm~M4#i4z0K1xZa>6PMaa+3cXl84AsmC7vcd?FNO>WURR#^2fM0Dy zO-VUwBiIe%-?wTQbm?$;V8OaRp8E{yf}+(JNcPZr28F%!M@?q-dZW zT=*rUB!+gv!(Qb)|hB;!`b{e77MhbYB`Z`?u) zBoTb@R_Pf~LPowb{L1$39Oh^Niy^eHDhFz1$nh(F@C1Qr%|KO=kVBMI>hR(Xx;Vcn zu)bS7W9(p(4fz;&!{H5j!||v~0ECbw0L}>ta_S*2!$1ONyhes?R6^B|ziJLlqPa6? zAwTH0^ndm0AbcBSNdpz|J5_kmCuwHkQyQYGI4_a=6gP;&&~t}nVkdR?nDV^Z?e}_# zo$qBGj&)nN8PJYtj~4s9{hsqBly{pAUfh7|c&vo+L3yyqd2U32N(!+x!Pd3&G;Bj$ zyfZ7_5T=TxWff3(qnkd!b*MIuFBF3uMDf$`P2@yP{WMvDq;hKPIp7D}=%KG1h9O2D zzlNKRxn*r)@`^1K>$rRMeJ*pi^;$B}Jfp-@)Cz85EVoVnrZd*)6WR#WFUZX>L-HU+ zP^>1yn(n_yu7@)XXfSO-XX$dlBR%#>{`Z97C~SE7yp@ty>l$j}x(8XP!0amT@QUmN z9GjE}92@Lash~QGMyk2$1D~;7pZ4Y>h)QRH0P1;m1(M@kK(j4IU#l_#Ge<23u@Js7 zPjM_h&1b&+IL^r6$|4n5I-q^u#^6l4bl$X^Cvm5kvi=^F;WU%h5pfOt4N~geL)W`sY^5~F0(5el z_*Wc!a!UrkLgJ-$cB;HQ4&G}ctiu2=Z7IyE8*@D3$O{t4BP(KWj6Gf2RW3-RVIr5Xp~DyJ&-t5Qjk;<=s=u@Elds+BG81c9mU&tw~ST+nU4*&fWB3qi!h7&QusL z<+GUDWZV*(_*(Gtmi4=;4QSC#$O$fv^8nxPMx>;q)O{{7l&hQ`3iH|E{^hPNEosb6 ztP2N}%kBYirabG9&a`B022Yn4KA}Rccib?b;a@pLAw~+ZsM^mgJ`A;xV3EP#OamF} z#Kxa4PKLPi+Jgc%HyN-q9Q9Cn;GYga7^jfEE;j@pf{X!>smZD1cB7JznKPlHEdR__ zKu_1wuT#)kx)uA;*r$bfW>~BROHGE&77B1jL(~o+;U~ohjA?C&3Y5skoc@O>3%fsR zM5MkJ4;5+TJ~ALn=JxqsRX! zeE#a(&xFBSV_;@x?K>kb)Dp6raIu?cUzJ-(XYKg+z@_!@;#c@RSAiG*A}8Fr_A#(A zhy1Z;#%v?%Xc{vznkIb-^d^5Mv_j6;W|>>|>C;1gg=A#&f-j^{Nel_vC|RG7GZ?^7 ztwG3|X8@>#=l+5lSbzvC7+~nnC7W%(m*YY!I~n*P%~=bJNd>H<_T)YW($bsJ!s2Og zqe?dW3MIgrC?G6u@_BD)ctsu(E#W)bP;3|2)> zy>7BVBYkn>`@5{k^L%kkkr9b-NoB%UR!_pQ2hze!UgtqLMy}X&8eZ^?Y(y0wNjaWSl3Rgd8A98`Sq9K}h28cxg z9bDyXKLqDJ$=q6yKQN$c343ja8=<(VI3)_afqBaj%5YqQ6fASAVaK`Dll`Q5GUqZ|h_HftGlgk3M%aszq>8@WXb8evoOp4Hh@ENJ$j4nuU-*%wXHl^E#e6+m7A+HDAe`#}^s>vvot0tYtv zgbFJwpzT>yg-TQ_=;D$YRP*3cWyR(B0UoI=)?$L8<3$P7p4fh6xH$Ds;D+}P&D=xM z)z0CY)83BHA&u{`agB|NN*)Y9=eSmvSOJ$Co%G3OQ}6(KajL@(bvsG^*QQRfB& zk-5Q`SEt|t^u0CAVv+LBcnV1@QfeUYE_=r5pi|MygtVNRr{b}MC- zi#zJVl82xV71nUt!2+r7-!aAw4_progR8`LWk(^TI`n&Z{<)=rthpNv9<3E z#ecU&K*MmOALOUtkKB??c%Bq@0a zzd~-_8CNt9vi~&l4Q|G7qqGp#z=03vvnKap5I|qVw;kH)irY$_6@E(urCHMf-;acC zWa@da8SV=DCNMh$YdKH53rWe-Z)ISA#vY-Q+Xeyn?f{bfUyF|fbm!;Fd5HZtZC~pv z!4vMbH0G~KbL`fTJ;<#!dSs07&%x?#4lGkhG0H*GRhNcu-7gdN6mE0a$O)&KyxvAm zSmuel{gk>6AOFiQH271$9={xJ2L()Et@CEw;jV}N$T^q6IiFikmf^d>eJj{p0R^tR zhs=}uzT#JCN=H{^C_wV`cW@b9V2K#gvH@5XwlUseLKkHSLQ*7H&H25+`M0Lp&lne^ zP{tUig6si20rgGu)+V$)rOd?;>ShNrs~$qiP|p~5;n}TLNH!QN{Gj|GFAhW&b55{9iXr?D-LmLWvIjs+`vzOBp9!v6t(;fg;K?itv(C}pi zR6lv)qkshL;-WyQT0Gx=S;G*r;I^~7eal59qtCh1DZc?8l4LL*GSN9=;}*Q#%UeRr z{BWi*lPsdHD7mN{Vr#76t)cc>D=_#K{$0hc&faD&=52)JhY0AmLGRyqYUDu;v8idh zS>%RV@U!MJ)vg}OVIH@iu8%$^&OYC^(WTW{a2GUm-|nCbQx_4~_c!9qcQ0iifEdCp z=vyuxp(OvARJda+R*G9oLt~_JMWO#VvB@%59xqV>ow09V*!SwQ#)tI0P~3F`uG`j@ z38j)?R$NO~A0*upqD7jz2moCLPMXirg1<)fr&49C#BK(k|KjVVGj60D}MLW9m=g^TdZ6ok`^ES1~a^+mdi%o`gKEJ#Topae_(FPl)}GOPnclE(q1Z{HbyWmchu z@GI_;uGuvPT7F0xBjWlWY8a|#3rAA*WWJe zrnz+K($B&_NVZMbw#Eebp4FV`d7s8=)8L~3ylSCPhgZfC82vLA?v zT)O;5;s;yGGzB(W2d}f)`T!tfd=-zz`j8Cud}7mfy8Tg)uTxV441{pkaEEvKque*o zw=dQXBJ=VFNTlu#T0sgsTFn^O#ITN#(Mt`mx9rIe<4|B2jG{50Ql)knO%%GOX<#D&&i_oaKm5=dyHBwn3idUk5*5f8iH<6vt$=(=1NkGz$S$b! z`-h;|{uz4|fHp!D6u8h!{xkRJC&&Q+Zd#sGH8=TAH)2TF9OWVRF>~`)s4LK`FPaq3NpfsRJ!t-=qMAEBp4@*OuT7GOyHgYxzY@Tf^C! zwZYjQbjn7hNqsl6!%(8U=&}x>%^~*0L zlC;SUi|;acW8Ds}*HpSGJ|DT#K~~+U=di&ykIM31&(2r3UcO5vJA@^>yYt}I-P~kL z&u?AaR33cs#U8S|SC8x5ea?N0Tl@Gis%IY>!(N9L+3r6Ramw()nfb1oGt)JL^hTDF zgI$AKi{645TifX9=x)%HeO6dlxDZ}HC~qR^o|~IX5xYb_QGlPi8*nV)Tv{6^FI$MT zaI#}^wScrq zng+d+hi3L?q8{Y=w-}!z*E3tkF+BsvJ#t{AWB_-_GG|U1`y|C#=cfc=1y^x zLtCTa#R#ywm|-JAFPp8nmywl~RhXQdoVVTK%go~Wj^N($@$sPiCPsWVsJ2-PhA{&l zbzzUi0*a5~rle$>p@V<*>o@>?#pN9cKoa8m$xxp2$2Z7hMhGd;d(FwtCdYHv7;}Pn zCZNkloY8qQtPozze{p_>5S2`TsPy31J}U+v>8BvQzK)jVMu3-tN|jmYKyt#*7HpeB zO)df_kIq!W!nfPL*&|DGgPuXx=tOqWEuk+ro*9gMDd=RTW&&KTjIV$K?WY0MZ(b7$ z05hK|EhDlT+fXC~G}^nnpBZ#K40JY~0CG$1I^>oSK$u&Awbq3Miu-V=wMv+<54@DN zzI*KpKptJe{ztdv?=Mmz#+cW*Zuxc$@S(oR{KChcS;{m0{ZIZl#@cUhK_F%hb$yXI zfcV=sf^b$Krx4m^Td&P<>DWAVhAzsrF%t@a($FRkD^iymY|z^L#v0(%4S=rnbAKLs zuo3lxWqCIJe8Q2^X-*zoISQD}#Kgo2^GBC}B6`*O7|}reDQiKH=1k{|NzJ=SlBC#CSxA7RgeWMKpX zYmb$}5ii_B@A_uI5&M+(j{P3&BO0W_)ad+9yfUo%^qytzQy27aqSoo(d;;@KQRMB; zhgPgoAhcqW_*E{`**o|Ln_NnY5$|x=N~yTKtx)!#t(#d1@mn=Wv=BPGrN7 zuK)sZ-)UHwqQUN#Um{&N-ev&8zq9pWj40PhE)H^&p!>=ozYMK7B(|u6aYHwD9mPVC z^Pon@3}hw5@d1g37RVJ7idE!49L8T?JT*al>l)!yOJ5|s`ij0tvI)45J-pmCBU{ix zW+#Yy?+|9;W}i&Z{Z;VYCIfNH_kh^VGtp0u3)TW%DPwURy3%?0Sb3l;=>T0x1n5eq zc27B+B-eYUl?6`r=L{XW2C-3h8LN3I`!&*a#8GGiS2i%o zW9#bbCeqT<`V_kE|43%U2hM30wSwhhi`DgSo(NdKBU>r85pGmoD?;w&JggMhvNDv1 z#;*}UTC&0*cg-DG;bmcAQ7TD!1Y!3|XOLn4jlYI`QJodq^57LU;bakduQw`d?V}vx zvnSzzdVbO}LFyL?3;gNa+w!%BB97%xh$)nj1CvxJU;=IaaTWDHvdA%*!H42Zl>*Pb?EHv!={@Ca*WSVf?Jh?O$nd_UE}$kFM}gj$ z-2kp*^HZjRopfTA#S=i6S{jA4XZeLAe=d%J zQKoS66|o8?-V%y}{%l)6l*|K*sPrXcAMgU(e^khZNQLYI&0rRpbx6c~aKw6(;)TES zmmnpD2m9=NQoFF-4FzdYhb%JeKnzzL+IZOlnP>_96oN= zmn>B;(=oE8l8;Tk0|-K7hWxES#90Ft~H}S_8l=rD>ad9NVp#P z*MuDb_$~f5JL2qQj|rf~?6W{{F|R=wD+V@KH8nLSX|RC<0L{>*K%xFG{53F{el~qV zuw)0ipR-K*Z*CsULSBFgfk6s`8mzrMtwm~{yP*9vX(St!LdIhB=dSKT1o9PT)FChI z<~Wvc_WjGm0pR+!b*;no<%Fv)tub7;1}8)A)?GAf_6iCnwcqq1ogFi}Mu7$=f7DpZ zgu}lbg9hB9ybj1?N5pQT6@lCzwbtH(494_;KK}2i3j&a=qmTbR_a*lUvPmz5X1p|v zx)gA6pjEV93BW%PGXy@<&;5D0n~Sb8fo$S+JL0Y&Gzm^fI8>F<8HMi-IgVnU!?^(b_^6JI7)K1HBBZQ06>|?MlAkW${6=P?(BtZQ7=^$=egFsU+Lxa5 zHwXR0$bUdP2(%o|9K;3L;Nce}E-*V#$3IIlM06(pb8Nrr+7MElC`TVwWpMq`v zlYI(V2r*w8sJ|&OLgOfN_g?pzV1TOV`(#YH@#S})FX+G1> zrUFs-@~bz4!2VN;0}9UG9Uf|sfqW2^OgMY$=L{XL!BYM!<4ka|X>Y zuz$yPWtAIuhXhlh+yuEM7$jv|n?eFkQ%ehDkvE{)2sQ|X{lD?o{}<9HWITq$=jSZ* zo11S1v$$978~iGC)Y3pi3vylRtRYUcpB94T4$3E>SW%Z&f0qi_VuKW8wKiH2NGXCeZG>m;!(g7nRlsGxr7jRY{{>90^six@;Z1!mqx0QHv6I>9Tx#r{ zH$wM|bu%l@cy99HKU@d}2JlbYcGISyj(W3_N#j2ERK#WJM$?zLjh#9U({;RY-4K^t zWwf03gKQ8JkLo}7a_bSMZ|7SKamVOHeT%lbK_whutNage*BN4rYC%4 z1=g_UI;`PYh9NeZ5{I}T>{z>%52-FP^;U+UYGo%9Efe!kSnh+a1NPAOBK6ZHTbSc8 zpIB#(%3o`b z=ec$=6>Vrpj{BKhs&z(kvgA#fIsHsBiw~~@_Ksb+BZ6een(*xACBiq4Fs`uf;(Y_Bs|+1Vw(6zStYQj0@9pjIe>i zZ9IEmtol)A+4K@I9w}W%(r2=ndbThv*U~;2(via;_p}!7jgYcc*L65sXtYADY=L}$ znx5$jx4g|+RMqt|24+6?z$!wULAv#)+;f!lJbZNf`J_nJg*Uo|)`Su``;(_`fEb1_ zm3-c>km6s|(h(6j!YUiaGA=xYLHYhqA8+#8K{Ek5Rk&ug-UEJ0vao=8X-Rr4N>Yf){&>mhDPL z+w=Y?{V@X*IzWwv0630IOf0&UL6_(cZtKX%tHr43OD}i3P7jCQ#ph|gIB#F{dBG{l zf1H{@e3qrQ!oOeWM*>wzue#q#Lfl8}6 z6GL!Yu@9yK1dACBix*67Y>8wue!fhRm<)wXQq9vZ{ZW6(!2WQ@fj-FdS*R-&?-{zH zDxykLCyvh(vclCT3M1N;5`aO~SkoXEe5x%_>m$o|GULE?d<$rOC*a{E1zWamMrgdckl)EC{go_Qa`se$RwVlTKhU?G@^Ub1)FR}7uw3Sc!Vq@AF zHs0RVsOQhgd~UUewoIR%=|GX~M^jG*IqEjVhxrNoj>1bw7;oFE>ogZMxn#CDser@$ zdYfc-f>QeZ+-EgOZ96O|3P!L1=35{WB&6zJdG|a>YRPfc9Rz!c8(>8#Qy?yRl^_ajZ8@G9>DJH3 z!T+Av$p!`LxlgYR|2WhC(O|56fXESD1B&`zd)n(rk8h$3BnMS0Xk;I8*8pGo4URWl z2gUPzJG2uXtU~m+Qlz`NS*7c?S)dp&AM} zld=`d`1n8>8qBweQY0UFe19~pu`r&wDH(0c9;{VhrbkY`ZIjQwJLSFqi_Vsi0y&f# z2;MFaE=PVnovlhp@iR{D|D(If$dZp1oRQ!l5{ewhkA!FJPYKT{v{@p=@x)z01P+2A zNy3F*)Hxo(yEhYBB!(|j(PTd0Cl5ghp7X8?NpC96?=&zzmRRqBK9)lOfk*@pxC%8^ zsc9J6{pS{{glo=!7xhI0=g~_PU}5lueX1LCA5&*8AJ^ zSHeng%l!sV%^>i;=x6?0MEz5K_VzrYg&MyKh~nS+%f{#uYI0ZkC?Ti=JWiFV0;CRA z+Otp#ZYYFyRZ2l4+As##@!JE4Y>vY7wc=j2{`B|Ke!X7fFJ2e;p{WYwv?+s&0L=_l zLqEkEed`^!&}rl+-A`oRVGsXQUBu>XCxI1Zz7bjtUheGdR6KY+CpZ3n#7jy_qsa68*V4B zbN!fifVzePtgmO0mijZwjY?Vs-*r`l3ce9w_fwbPf&C7kRIGGPr=Egoj)!CRh{ZOtS`zyQ{;WWPSaqwaXmsB{IURn(P^TwP2tC1SQEQb3aktP zP;oW`o)x7ky3KkCBhQL26I3SFu^VLA8^E9vZser-$x{e;+)Awuozjqq6 z1R#@78G!zw>TA@F5;$^xMA`cx6qyS3f5NCgg&Lw@L5R&OWUYmaz5j)b|3b!pAp@M! ze<9;X?Q`|Nknvy0_@Cv*e<5S-nfzB}{8weHpf)exFmm z7F_6H#s;!%MAVoB5Ky zUg{%8mW6AI>!hufPkt}09YuA{k>ftbJYzDd-@??pa4m0aAaU^6&Xi_@ZB)5ejD|DD zot6Bz^bfwWi(EkgBawh-@EcNdzrRdJgDbsL`njGo>nK4e;BgM$#(}+VYxlvm|At;= zWoTKV_~RXnI#;{2vcKx1|6;EeMx-9v!r-$}igs{i?vC|`M=|lv>Yt3vRP+dB>sQ5Pt`!)LF!T zChE<{cXmlU>wKXEI>>J;zn<*u=m0uSbn`+?X`IKLTyOW;Ot*y`Uf=N+&4h!M`gH4j zl|6fqdN2+af6Lg~W53e++mfaoV~cCE^P60FdZ@vd8S$XiMqJiVWP zs@SQj4<{*^-nGL(^tW{$0}k5D{#I&pA5@dr$6PiZ+OmANZU3KehQGoffii35CPK0e z8sDd&oJV|ym86I@9!@Nm+@k5cv@PbZfBsK^oX|@IVzC!93}-H0z$^R32|8UpKdCB# zo3v2d+L@YrhId;t^EwxN1i{$C3OGVH;EF>q?Dh*iDo+T8A z7vf8JwCv`x?Sj%%`I+{gjohi~y4;D~%9{eJ1^QD3X%077yH1nc;e% zu%_)GyX!|Uy{QT&-rjKs2Cr6K59u!ZP7ak|A3FI~hZ5UINmZvMVN7)gt<1vt+l>zZPC~%7ufI7cSx0EA{WLRq=n&)o%)Th;j9HLd30lKu zW>f|v9I(f#Y!7bM%qXYk8m-aOLW^Vj#t2>vezqnFt=G2GRc=Us({~pa;*lp6$R#V& zG?ja!pA0LK#Y2~T^UH-{y?<_~T-gfHjFJKSkqvq&oL$o|qsq)|3r-Vc4q*mk6Zy5A z%8#8@I~f7i%wrSM?LejIK7RYc(b$8Qr{y9}2ygOIkKYq=npX24oyoh4+}GtK5_*n1 z4#XPzg?f;F3$tw!vA~l`8L(+lXG|ie%DuMjq^pcSA)7nM$-Tay{oj zbE;kYX{8`qCO4Cy1lCzOkb*NyV-YBv?Mv#9KElK0zA~y*CXSgK-{6h)#>`e&zg>+@ z@2FJoJqVw*>3{nya8N)E6A%}JpIGfJ=+mv7&s*&@Y~eD_z_f+yio24eF=`Qb?@cs zS(3*+&y^h;O;PC>BS@RIq*%AK56*RRIh={euwIx= zlT>35Xbuk7adxUGX{_8IWxi9@r8TLdcEO{_QLt5Oc~IZ?i$RDCW0?AlJ0FEtqV*oB zsM3t46x&A}zJ6o4QO~2-c~rJcchy;bYKcS~l*K()a#zVpm8U(L*Vm@1X|X-wY;;E< z{=32B?H5CG`7tN*R?}POC!B|#`In^&uAFro!YN|+_+l0w>*VD=Qj1|!9gpCIM%_Z` zT(ux(N7_RFBiKO2MHwPJukQHfDxvbp(Ua-#L-#W*FT7RN^t(gPGaqbZ>0Zv1T{$>E z*V=nP%ywirv31)k@!p$F89mNcEc^vS|7F2g!kDJhM-^u$=_ zV|)ce&!q{XP8Zu8;|Pr1H>SAumo1bpFLd@=j@d*V&H5~ML~6CX{~3QPW0+m;k0{Z(2!7@;NzB~q4(BZ zU}2OVmsicnI{9QLg@ZHx#bi7!M*8HXQymeVoBR)9aMHYkN>!8i%ey=ri*s?< z`g4-SBa6!uEBV%KYBaB$`6h~%CtR>$Tx|sdQ;$QW&fAO;Jo0vrl??VUET3l6U7X&f zhjEi-vc&Y)S$__v*+L+W>c-2fU_w?0TFV3^@jE*@I+8fKKGe}GA3OTw`~q9u;DP~$ z!=Ctw$3>9iwA?~pfS{i@##KFUY`S+|M{T@HF{gb$yW6MxdYKiB$@Y08CH`Dad!N`; zUJ)I9jX&Ax>NslsnF(`#`;B>NqXV7k#q%2KCL@ymH%u|MeXsLLCCkC?%Sw{^ZS$i_ zf>f}_Phr7GkEOd*W@@p=jZnWcCV)jR#;KCp-g#<)&8ld-z)I~`+!y=`x76fkZ}|el zrXX>e<*{P5HtRA6f~2fch^JL* z<&Qb!@rZUFjyp((#gjf);UfBj(@)*NOFQ`aGHPTH9FLIuB&W}(Wt5$Jq~c$^F?HIY zpHL$HWGO55a+mU>CesPl_nwo})$R=TPcqH&`s*akf)j~vANbyVf8^ZC@Vy=ur#aD9 zzhc6QtK)Edcy$zWBc483R%~qYeF*JvmnqM6hT9b`M?VKp`1b1BoXobQ=psx|i&4rF zS5N6Zz8=OhSwz9*dcd{lyv_2jMuVP%&ByY(t7KSV1=0cx7HH~SQ{~LAyej+8uyWD9 zdq7r69XKW_S!f`t#K#lp@q@9ntXZ7$rx_Jg$EXKHNf2fu<)Hvr0j7T+YIMoOzwvB54x-sBC`|6Moz^J+_C#()sCO zVgP=xlp9_J{u9X4 zcl}EL@}At~1)4cg_5QJ}dMRo1lJ*4A;Fg}~V{`$e(u#9OWH?rd;h0YoH*YJpXd8`N zH~2DLmcqv?IrJLf^;CGm(gaB|i!bQ0^PO%fdA!+*PO|R*+(LQ#7EHv;ZHZ$A#}MMz zl;)JuyZTU4;HY)bVB5ApMIZ)#rp zGjn7m^_E_l`MhUOI`B{MZiMoT?i{Z1ZrP8LlrGAw10f#WcA61FdM7HHr`-Iaimj55 zhgZ3_CM5&2K_@Tc%`k^(Y(SfJN?svO(1RvGP-d3ESvlG#N$lw&HvNCzT}5T2}2lD_AgD)MsxR(sPdZ&&_qLS?g_H(;_X;3WNK*7G#vS>IV(xB}|0Kqplb~Wp6e=RfJM8Qm8M(O5J zhVYi6(X_&{Ej-a$!UH92HV5lQNzN9%-F6net~2}<`*@PIcW!*uY58iLHJ!y&E)VC@ z!y%&qyf$t7$3ioik8!g{>R3g-EOmbk_giqUoUVW0yL?K9sb{PZ{%@YLod-8k_mYaifbqRtt?$ltF%p3Nzpg4HaL2&-ja^%6>fqU z;^~O_Iu$d{5U;*dJnXvLCQkD(gn7{TvpYNHjf|L`E<99XFzM7fp39g-IXK)+LIT6G zyF008yhK)Dd9GrmX9dFX%C51+e7B{Tj>2w-;hsjX{HX)kI23Y=-&*r+PHbc&mP5Hj znmBqVT#3+`fuEXr<2zcC?{4feBkF@GoEx?-7hDY;c2s@*=DdUzsrm4@!OI|8NHwj$ z6!N$yK7R%D8yN`y^t=3wnY5>q?;13V)k%ye;#M>*FW9_nhjjzEn)5wOgtu;lZ3}57 zccao?3&Rq<)?f*~vHU6{Dy5;4xtLm{;$WYw@GgrF*H*oLS-Q}D);qLJan&fQr(bxi zbFw(f*PqMng@f*5h^dNgd_S^{ylo(^^N|>Di9t4w%bgH)*KN+S<}A1Uu$2JS@|c=* zUcmq1?9BtA-249VyJRVf7EAVXT2%HVd#DqqgtBju93;!w2ZKqG%GPGdHc3eK?CWI2 z6j@5v!Ng==#yW#BGr!NJ&Ux5tWYaO19AL8TkIl7PbU5>~>H2429 zluuHdO?7VwYA3rnF4$#S2I11Ig>pslszFx~XdrMBui-de9%<2}FO?Wtpx8#Na=DYq3V$zCn+6`JrIMNnri zxVf^HEOpD`1M^_N`L*8l3nC|cI*Ev}&)9|lVlWI%^}zayP;RXbeS#TV?jzBQZ*wOV zrSRGWoKz)E*x3&6W)I&TW^W1NTFjlcg~ZB*2R)R3i|zj{U+}&I(g;OZFp&C1gsYD7 zKmZDq?n@=tzn~X)8w`KcuSqW-triJ7`FV9%s1|2VO+a5Q^=r5`?bsD~eRdy@)weD) z)UzC8&L+delcF`0Xv>BV4-ZlzYaG40bIonntGjc7+qW^wJbs^iD2^yNCpLX&{&T1P zUQ3s2U=H%^ZJt9TEaz9Ox+ut7}S z+UHH{0j{QFN5Ssz_aXDmop|FVsy}Zk(hWF-!|it6JBDv?M{5TY-f)eh`?FvgiXF*~ z)9IrcsCetQwaN1f*kF~J)olHJ-fMTT_XQs8CGEr3&z;=&wmWDgs4O*LD!Xt@S%AY& zWR#V#GM!uMYi1YK)qohP)igs=eZ+^H%!1wU_=c4UT(m3WQ}S6sIW5Gge}@1tn>E-3 z^JuW?cb~_7qD5-4gyvkaK9O8Xtj4GkhOV%(mm^l!$YFRq=?Y1~dcyV$Yp9M?{>E0TV@-&&10`b! za-y23;2Y)BLqiY6QUvu*Y#39P_X2Os88wf?)6@F1y=sXsnK;>ORKK$~xH(pY^s13J z#9fNI!tW0fZ}6V;rlT8bmfCQL{{1#;r;|O#%xiHg8xKN;!_}A5KtLrFkv6Q+v#-bQ zPIQka*v_$>6me%xvblDFTD`Nr; zciE*BMep4y3>TZ;gZbM7XsvuBCOR;71Q%xMR$J;|>E(5!T%X8aw-!Yg@;%tF_XB9{ zEU*nv@fG$(-@EQNt?tI5BVB4lbSidi-&Y;vTQEP4-pGLC-GgUEoF%@-aMuVE@&RvNFuHatWJRg@_G zj@OY?g;k6^@X>l*P_KL#v|8!Apo1OS9LNrgeKICF;=k2-Ju zx%$j7n5WKb1TIeMsV>@#1ZZ=+jq!$xRxttC?_0pAvOy5SlGs0(hxPE)Y6DAtv*f24 z!EcW1CaYTocJSMV_~ZAmB7-^&hXr`?sHz-I9KzA|VW$_mu>a+F)Q8r$sw5P%nvG%sZD zUrUlL7H>ByS6J1rR;EYim3a`(2Xc;|XE!d$3|jmo*1(P2K)a8|nug^R)RslZ;gwfv zyxKUDTxz~pstj$&Tg3$L-6}S`2kp|dbFBPA+lRT=Ua4T^GhTcTlc{K%y?ZIlh8t7Q zJTB6M8xVmZ&k6<%)z;#UtPz58r1nRnWL=L|=k}S_0$|=-g}2|KXg{=Vr#ZX!nX>F* zQ?KYU8UBW%R*d;cS4})Eq=&F}sY;0kMjW@-@LMjA-mAX;=_GXe=_DIEz7Xf=D~XeP zryefwpfyom{0g!sQJ$63+d$^z^X{oBY-1{VJC7$1@y=CE&$(k$;9&xE64{-IoD(RC z<%Chw+?!*N9mJa9fMw^8`?k3GD1>Oe|4qa`93IuOs0I8rc(oZ ziIIqaa_x1jqk^T>Om)}dkwMok+WmuE`%2C@%=~a|Dd3`c&&Jvg`H$!IkIQ0(Me67c zQ@?$GZ6CiEGU^yg*U%oX76GO2@9#B8OTW#%Eujw1tpj$7tS-s5?+Ztkt|N95PM3(K z_zhSJ#mpB}?*5ROTWHYw#Tq$SI_wIwOdDH};2a1ds|_c6wR1YA2I;lBW!Bo=t{9sV zUl&3N!~{oeMNB3rH~DiuD;<4zKBipV@XdFDn#&FvE1LWCP8e~}##)@i9)>+GT&^4q z;l<3U*XES;Jtl(CjUR+=Pxnq%!?GH7v)Kti3d6lI)|BL$52sja2OR;sDnOTLkS~9`AXR!87pskeKREnhZB91m)Md1m(09JJ`_&8I@s`ggoMFHs#)*Q4Un;G?!0Y- z!~Gd0!o+-2JBaXc{)Il#fY8iU=lA`$VN8V7+W^(NaUwztWwv5Au!dom4yD%3H$Fx^ zjkfF+UQqKx_m#3+eE_g0_9* z$tz2R^B8vHS;wrysC_-ar816@to(6cxXf}L&Fkr=`quX^F~<3YZ%{d+X3_8LP{zkUSW_zxt^2HPn)lvB6f+VET`^O*vqND{-;y%2nDm$zE{9) zbq=`CE*nc_xQ1?AV9D6Ackbta2zPq0I|+U+`ZnY#FeD)sBeHAkg>TaS)aX_hWn6+*29 zk=LxC9IWX+;I@Ko>pDr?`00Xbw~8MlHbyfM0$zkK1+prP^8TPXN#j>NDp-rE!bZ>y z-uQsSQ1xpMARQJfM+!?D3?jaXe6T;vRVH0~V|6qn$I%nr;N)Gg!**u+psz;tHuIWYq0APP9bt(A(fk_}Fj%YzR_mH^K6hPnx66JRS6#cy z6J1|3VV2tH7Zq1qY_gTOJ@WU_o6|r*VZx~+QS?W}cq$U8t6)Rh7oUrr+ia~2NKFFy z?1Uy`8-p?jM|;+2<@JPlf?=>lS=OTC%n_Ey#e2AsD#26FY@bCbOsW0G669P^u35xi z40l9{EYv2b5NqtroHDo@BBX2h+-O+IVKN&1`^K6ya?)$eG6^e4ZN**2cm#{Gvt>oG>W)3CzS^KO0n^PrUm#g_xPy^kq9Y)yuA{jB9-%X;5#hPHoh~ zJM$m#=LFj-=N7)IiP44KG*;&tU>pUb#o40CMPGinfA5Cr1Q#^8y1VVfypR*&oUWzR zGWiCz=qiNT$C+p1m6#+`dWk=jeM<~L8Ax1eKmWbDw7|J4YTU@_xC}3CWzk66ez9|v zb`=Ymv=)oFCLp_o@)9EQ!94L`_%?VmCiY)h0C0E@U4-p)ulS9(Elt{1i<(D%1drp$`CXj}0tN2?HX{Vj5s zLn%SDJM%d@tkz>C-o?Dh2;bgY>3emOj;!y)7-Yq$ot3L0QbpdF?>AG1oxppTe_d)< z%Ne7P?|;=&bu1ipYr&brQ%XTSV9kCle}q_oBuvbA8tPA%BNbKZ$b$zPd~qgHLIMq4 zYUF;$_hjGhPSZTPP2*Vuzi)Cp;|wQGZ%9B+9N|EErInkDge<>ELT>zF*Ua#DeQ~@| zXT1ct&XEU@4b|Od2#v8EI`L8AVo5f&ZLPaJhFyJQL4SQ#Ad0^iu^;%cn4$N>_LoCoiU@@BSmvUaN+?Vf)zNEZB>pwqKVwYDqm!Z44cuCEBi z2DJ+{li>QaU`1N!v-_U6{U&J0aR2d?@{s{3ClEOM5q7`WxrSIU@?*zYy>kmHwv^fO zynrU>f?A$))E8o&okMYE zrGl({(>1C%2PIW7-c^;HD+g7QxJ^=N`H9>vc|2TAl$v3f!u?tGh3vZW0T?1jvYaCe z<$IY((*om||I$}qy3z|rQyXjw&}neh3)F>&v*nyN>WdEwKmI;>%85{1f=#`U(@{MF zj}J38oqeM`5Fte~?xjcAybW-9nF6y64g)igLOkaRyC|&6k~NFX;=h@Qr+N*oF{PSL ze~;%pcVecPYz-;}EWwU;r(!G!g($|A_su2E$J%8Na@8$QB^Qofqq7yXei};{6Mp^xKd^u2$$3welYJ&uC4=Ls=}pcC921vunPD zm7cKCxFmzpD8$^F>4UCNBzi3~xZZ|?1&((uFPxTA2?I6tD!-sSGO5uE1gb*ZK^)6- zvzEO#2*9^alP&Gkk?2)RF>& zO3M-K$ir>)#xQC$d9a9VjkrH}uGl$2)!zVr;Zs>bB}so+1+R2+V~K>zDLagMBG;}c z=~74wFpj~d(5XaZ15AqInwB_U&zr)FxFqQU0WxG99+``JUnOupYm8F~>-6`*Oy{j;Pj zKPpMYdf}GhY$8@`<;Zd$R3t06ur&llqq3~5@cmBq(34>@GAwsNp#9e^@fm?^eJ52p$8*ACHL_w*S+H^3>FaCz2xmo3 zA3=T9FNa6u`phe``igD#dX3fWKQ4*$(qE`+iV;!&miac7mmWgL)6dz=93idzkybE= zkt+nXGhP}GyLkR8sc`09i%D%+YD%{1)MRHR!%74n`)0w=m}56P-Joio_^|qsfQPo# z>HHEQG&KDY<(&Kbz*vH1LoSAVYvYR54XcWoj?%>Z0QWN%)mwM^y#Ev6JB%Bw-!OIbR#wQ#PXxEuBnUrkF4!azO=2ZB%B-sBH#j7WwVTe-Y<4hPzs^760_eIyV>en43_)5&GbT4v6 z#>Zf(P9?wb;odEz7jD*K)6Wf_U{!1W_8_^$s1=Tc+kxJ@i#lC74NU z>yLON3GAJ#A^FKcB*k431n|>!`7fjrioU#tY&b0Qx5MlVd34k9(l`%YLs5WCyg(8_ zV?f0;NLNU2&`|BqiMLzQhlR zRLRX{4UOUUNJpy9*oohF?Xk=p(n-<(ufCZ7N&JpYAlHN%i1NwqYZoBq;SU*NA>OPd zUBqbOa`}deX71iY&2TUdVWG-P$hBiotC{%2=uK=GCN&{mEBxVL&(ZYZus$yqWyxZ@ zh)9%Jpj*o*2C(&Wa}j>Vj@>4SZ@EF#oRosFNv{zoqdkkm<2`L0H%4ld&UwvhBg(&$CDB;3~Q|N$@`p_w)WOE`un$cW_Y1Gpwh7H(R(ORT`bh$xEev za$jrJ52K(G)js4*sklNtjjrA1r(DpqcX#X8Cz5ryyf(g4NE@(ygNE7#+n#w&-;=)Y zJQr|wz@50{CB)No)xE)(hdoKpMk2RzmzJg{HwRru?rwav!PLvx6c z*!df2@8%cAt@pOqt!gdQh82t>;*O@ktSlc84_ZNOolr~4W?Sc3r~1PF#e@=aaSr4?y9$&dh=jiKPNKAS}O))X4B}Ru=ugl$C)YI&-z7V2G{~%oQ(ZKP#TPT)pMj z0fB`E&-oq#tdIuQ3(pmRE?F!Q|d;|3F>*Y0n%R}HkVdHxDyf1;PJnP?`&=K5}}mF#pt&luk* z1RJvSZ$L6DueTlo{#VEsXoOGV@Xwh%XtI4Y5#S;oozwK%<~r>(!F57&{HQoEb^Q}& zZ98-(Q5j{3a@T&1al(CkxD`gC|4Z?DUv>yIgc?nta7%$%Cu2tg8GYqITn9Jh-LjBVa$e*K7T=Od$@ zoxWT>O|SB+%6zz~X?I_-5dZzgjxUWn4gR=TwcK%c$Lqf)Bttbk#n4Lwe&GnQ z&eKYco-LF*Yrc_hzMsqFn5Q+^(|Oov>w6A4%l%PKba^jkrw!OQ#XNF5 zk+G%)O0Gi-u+;IxGhkoViP%@p>)(z~R=P0`hBp7lnWq}oJ8UxoWLomB$XawvbQ^O? znIE_=m&xrdwiUWI#QY`ipx>Q7-FPbmcG;EL_N2~DphzUHZZ{;_4dI@B>IJWG2s+z% z>lsJ#HebenMYR2nj9TJJEp~n|bj`fo&~=By=Ai*;bx&Wd+D4At!36z1&a0&efQ)h= zZX3Q9|NSvoe=QYn_c=i}lkfVIdcGM8?$CRaii~<)e-T5*Dx_v7W{Mhkukh59rFUg_ zWiDq-B%YHWSJ`gp`lZmW#XW5C66Q}EO^$JgUaURi131*TC(`NS;I^7fQUZKp!h?W- zfL-8M;-%c7Cm*JO4GSl3Z-p}j=w@LdS^(L)`hrVf2-sKJFp`I3YXs6e8WDSbaC03` zz5FMz-?c|$4`fCyn@YutXM{Pa%@Sv3T&^|W6agFQi|Q=4b}maIG*Wk@0XhacgK_)q z=AtzOc7xZMyk#xcC&Yqhmq-9NYTKb}--92;MX!LNYxY3I6o4O_w5WEtB0leyxSfMz z9HPpE`Rn!F!j}rb-e?vkZn^)0t>gt@bLiWHJ)nP&@XSi(Nk;@PrJ6Bo_|>j(f?T@jEiS=4jh-uv~x?o3^{uMI<6r=g9eW;FM`Ln9kV_Q z-A?Yi-GJGZq>Z`8INmC4=|yTJfrjm*zrH(*v!xq7%AYh$dOg04D2aCbxp~euzexE}zMK@Eg*bCZ|Smf3f#beJmL(?GDlh9zdAW$lz- zrQ1ygAR_z%82+DI3;MT>k;~y8hAD0*t^6dO*8pIa>*zrj(@9bvJ@X(ygs+T0Vq@ zhfl%HCF5;jNc9_M#@8cr9>~dgyr|%?D989a2+!|kzH?K_uvPchaW+=V06#S=CN{R+ z0ynt48DnNM14v$W4arxZz;`Uy7MiLj*Rh>q zvw5$J7req98VsWL==TY-LQhWZrZNr+UdjUq58czzVBhY(=Tr5^{cU}UM|T6|c^Q9+dDy@VFDx;^+$jN%lrKD7x|#u&z4p3ZXj>_ zsu?S_P*Rz_CGEyZ*oIp7G-6+nsyP@33u?2Ag6f!a)zIQeiT~;2$s| z_zJ71+@%=<0PeTrMi1}~$hUebGUE!bxx!vR1@WgaJ=XUHnYxghb4>F?}3A}VqF zGdMsjfZbfV?-c*O16*%fR5C-LyATKHcVG|G{4KDdevgEx#OlRP)eucU_)VTHN%TD}$zyKcLuyp_%TLu7UZ_O}AA&!onsyFzgz!t$z22%C$Wp$>$ zQPS8g5y3Tmt|J5N-?o;$P{SZSs__O*{_lu;rF}{ulsUR$ z4<@5xCvcp>8fw1t048(S;|7N6?$!b5a?XJ5RBJ4@noI?E-A@AS)R(*fE&o0MW))<4 zYRKqLMLTvuRO!+N60qsWTkQeZ=xdE%1w%%`$|Wh*I77zL4>cFF2q*XFcbRZ$WgoiU znbj@qmgKX;7ROP_>I!W(VA}Ucis3@(8=J3m`ek|r>w$WH3e+=4^-t~WIS%kr-2PO% zdJr&R(yc=8ScX)&$M)keS0;{)jkVtT$%Vh&NB4`0mSg=FBU(H_ZF)h2+#u_=vD&-Q zXlS?YAW-^w$^UT^*b#rW+izkoBeve9bqaEvYteT$ePF<3&bUMAN=yn)UOiSw%K~&&20ZqQ=k9w1P1}Q=sHh87(;)zfnSSndUY{^U|db)TH=+66mY~JW>39t zrsUKese99b)bl5ro$l?Uy0$FWFlF%e`JjKbxvXn*Rf97AQX`mpj8O1pygn)NK4hw= zoR1v^kjU0wJ39^g{!{^az^J_QU2(D3nbe%oIRJk<`FIc8w9RZW((95vR+Dw^ntA3) zZCEWKKS7RM*G&4*WfCP+-uHXXN6X@OweRST{-9gd2Hxx2;Lz(_&T^cBzq$408< z8Q!udkhZZ0=6@Li8#M)9gnw7J!**rjNQ~P49(e{!B=KSUB7$aRpw`2N!<^o*N zBI;6QgSP`pTduWXTC>KaAIyadM`ab;&%)Z{VIs~2l>xI-)8d+7hy8($!KVy4h8fcH znIrF`qoe0REXs0HKKrMX(3k~Ih~}pHv8`jntsqrV*LBK zJurEH;T@(o1FJLIU|SMM3xkC*j?y3Nbp2YR${QPERIgR~-R9wPlXG55Q1pmhZ_=`L z237s3W|(x1#9Kmb(^O95P(g1|c{s$Bs@3zA&Axj$70h6>Lv~%U%aTp#L3f0YoORU& zQxi?4vUk)#(qL0ger?8SZqNRO#gy{sGiuY+CE-W@oagvs)YrUom2J=GD*3M@lr|nO zY7Skfoh?}c1C}Y;_+m!01s#`n`m==AX_|dcx(58&AY9Skdf1dHy=iuMg5;vO!?sl0 zYivlMysc7<$9~xC?eS!#pl5#$_urz$iHX*1Bqk9;=Cn@@PEVX@oF*eL!c5!O22t-| zNmAJ^aMvOy7bR+AE%vSWl5pTrsp|EyDF?v{a?4GO`n@_V%1>FUZW&gKB_T$22%Pa& zu|aaBS#TSlV}porSH~tT!=9YE+qjH7g|6X4_ZXhw=BD$~0c?G<&e9Z#g}i-y`qS zvyo3(sE0{zRK)9GBc$|@`@P(!?k!UemQu{2u1Yh)jB+kuMIGdSoXO;qoM*nKS#hZ6 zQ2L5=-=9pFZ z>aj9hK*W^84VdAuPOL5MDlX zu7m(sTS$9Qjv-@!@Rud@6Bf{<_88Dk7)T8kS}|daO|y4-tiV6QbIyti>a_?72^B5* z7T=s=nf{iN3fHKZ*0aR2Oy^yP$CP+1xq1;uD)ei8c(TfamgE5zm0h8(G9QJXFgkP@ zwAt{J4DjRr>(Cj7kwYoEvH`G+FJDq`me4N64PG`3m|Iz#ozA4@rpAKzk;Q!@UtLuHQMLB+B!Hma{)8pe z$>NIujEa!(8|%A#PtMWhK=C{DDAAVs9~VW>61nvov)bQSJ%jQa#~FGf9;nj#O|J^J#BB?s74iizcBe&}r)*y!5L*}ljUntgjd zaT*rI`tFVl6^uQ#_B#BuuPb|bON<4$^3Qs7^}-9*VZyYPw*tj5np!u4oL5|z(cDyr z?!V*S&6Cvhc~W`~artxYcu2)urb+-=WK&;+F2v6Mm%;#G2nHNgzl#b!$7(VPh1kwS zLFREKB~xOn`fhK+H|g&Em$A-1u>i)P8jS4@x~1pn|cekbM$u9C4ijXDY?@KUq0va~hKzI_K$ zd&>FImvse~H+SaaROX^Bi_yzE#`kcrV*4vCz&x%9=xHQiYVjMx1%2qNaVGmSzZ37A zQM<*n%LEy3<$GOqz4uvHs~P#CbCZ;ci?M$bHr4MGEuc$xVPh#GASR0&Y^}vUIJ+zK z$W5>P4BZNTJz!{|?BInQVAu=q6w)pN!yfpfNNi-i58m`lzpcRPaUDUyt0_~~Z6;xz zMa$nA7^=tikl_p=D`VH*WB0n{{oN)H5O{G=-bDqqV5;WKV`Xm+Y1h%`nPhJI^ zscib{b_~CAx!h_bn^32a5PZ<|`Z3?YPjN+U{m$Js6;I26-p@puZmienh|}BN`1+vK z;L(?I@+<};)$NT#W4;;fCkcNvQC7aZ@HEfiFTcO6snlFxWG9hViSIKRd>#?vH)kt36r<4uw(ZqHQr zv@!V-qtSQ)1^aeGY94xS)3wNVGl`pA1#RR#Dnd^EmB5)O>hvu>)%BE@Itr^DM~`(? zYpq+`u4kNkU1x-(%&mMi%R|9V4=InQR*aIWPi6Uddw-ZF<7)#h{b$PsC0U9oo2k~g zZy*j~83d^WS%us4Cd{ELOx~?4<&A1&pPpGOc#6DTl6eyLhy3xbS=eY4bGuZeBkwtp z97pc^U0j8+D7d{^(j9mEPu0>4Ict#{vXiwy4-k95m6`yvt=rQtZ)Y*M;M)gwiN<&* z8Z6&W8vnq=dG4w4=cuSwg%qAn1>76jQOjMPAmA9=m3^3@zRCRyO6D$~02yHZfAdB* z6N)!PLFz!`G>MU&(s)@3bJ*o{EKLB8?UH5k~!!U1|H1p7ZS#vs4N0)>~$xscQM0NAQWAoVhTtn&=W5rNHQ z|5CcbIOnjLo&1&WJv0B)p0WqjpV7I&2OKb4QKc{jCUM73T~R0s3j%d~;4x=_5E!hV zO}3F}-ERhVR}XvMP+}Ys?gcxzT)x+5&szMGSDGCzGw0e;5wfq0-GkZBfpM402J0pZK|Wrf&0Z27eW!5r{x>H1Cjb=qA&ZVFL-Ch+07?b6 zRZ(}`xn{VPj4U-1p7xrbVlxfxECqQx4G_NXJQL4wFy-YAh%z}bV9V>}A2qq)&HbRn z)b<|i!@BV&IP>Ez5T`W^p`Zq&?!FJ30ROEQ_$}yLiDEJjK={Qa7|uB4{{ynglTXBd zDyh`e)Ml;zYf|y)RVi`*KN3OU&YyZfiKCYw?YM}Ua?pVQPd&}l4l%lD*Avh^KVni* zR>mlRB>F?Zf?*)%wS3np?!63PrUnhqJ&b<1D^UXqqt@U2lNBL-X*LOS@X2ni-(Kw=pe4OI#K%9~s*G*`>ew~U)NY|4$%i$Q16gUyIN8JPUTuQH;5ot>Ho77>8kXK6SADE|(W zhPJjgKzk`eUm!6|xZKfr2BlK|dJpbe=#3``$RsTWj;3cq-+yDEqDa=^iuSbXV=;!E z9~uJeAklJM3#6`=PiKQ{V^XtG=b7}MAD?J_MptfXiw=QS=t7^$8fmU#G=LBrU7FAa z$klAGo9m4>Ql1WdV1oWeqlckAqI1PP}IkFHY;S=3~+)RVX(yXmTFm247RMPJrJN8)I1 zSD~J$I+$P#j;0Gg z_q+A=^@0qmhZ^jVUOE2gpUSBTQ=(_3)k(=KI42}n5IwBz%uzGKy9s9>0jxf1K_6HYmlI2d|J>v76&EZulxy>P>rEV%m1~6n(>cF zn>{Ys20J-V5-2W^@xF_H2B{BgU@G{mwyDhVY`P4c5x(Bupoll620+4}|69`wxmxVQ z-&g?u&)H^=K}zx)s7SnrYct!1RBTfZE_ni}heWV&5j_4f9z6b^E5f$1x|`L=?7K3K zS6dnD3`jitp(d51Og5I9d>IDbn*n3bmODZLEwp~scjMj-Tn68fZV4PETR0HFj+8Sx zj0GMdY)A8E&Mp&K1}8wxhc59)vhg;N4f+Lcz(Q@$1ey1i_-Yh4VuI2--EZ7vRMv;r2Q z;BMh1mdgqZeTB`}@0I#*){q~xfc&RU5n#HF&;FB2{8>F~gLiBVKz>I<1xl5#v)G2f zu7LUl$rTHgxoeU#xBjkO7C#SQX{l+}e2NXU#<)Cv(z!M1HSGTGd>Cj3E=Uv%s*`X6 zld&t$d`sEYh+v0w%yID8>bw8kIsm0fZsdRu3HiFshunj#Ri-v@YBia_R$@`JZMuN^ zuWYh6c2^gjM9dCUL{%(pX4fx17DE_IZ-WNjb*)r-Gd04LLP@9}m;M(&J^=bG*8-61SGM?Mj%YGi4B%+HouFO-zLEPSlW%dJ;yY|LiMs!X z`_D{G72aWcjTo^9^XjkCt;O_~G*qep42o6&%9f>Dm7xiQblFQ?C^k80`m@IH^z!ab zt#$GEme{YDl2j7>e=S0wDh(Cg>MnxUZMW;-Wfb%}@veIrPeEJB%G^JZ;I;DY);?&k zZ?YfMkydvCG%fXc=WTj~3R|y70)1m|ArJrbDNtmSE!b=v+oVn*->tl@yVNYK0Ck|F zpf}%B3rajP+w}%YLPe;AOy1dviL07s-w>eB^Mv^}T~Yly;Qz~HgKr8UMuXLs9`aka z9^X0wIi-Ks38iE9VD8?K--hUN(Br3)HtS&q%(iVWx7^SRE+-pul3D8hk&;UR`AJ(^ z?j1XSW|01lx$bH~sjY^&MA^KTD%)+600sR;4z_2{KzXwqSZ|qQMNk>{=ysaM86vew z?5GTYXrS}^QB*z|%+a-fUTvIb=cj_2Do6+(<8!_5kOicrAnXTAf; z*sh!RWh*2ZIiQ>OS3Aw$A=AsP4duT=e4#=))UAn6__sjmKZwQV0m}aIrGAD-|Bqwy ziy1G#@0L- z!omwR1;pG2ejSU|e{`KT*&96epQr@P%6BHCS?hn&5G#+5ZK*AR-((B1XI#rDpPWPF zNv&{6bh6k#jjWQ6?0Q@KDqNymPe7`q6p-r|iGS6z+A!M*YB=z4hM(rXk=zVahLZj- z_phd=wiEMeh!=DRe!3mcFpArB>a3wi3NUCe3`i$OuK(74K+>DH2wf53cU7z3^a@u_hN2Z$M*kI zZItb>U>J7&)CLuRI-51&&*B znXYW2T;bSO`fSHeSE%6{iVkh~z^5^7vk0o=I3yPja}FHvg~O#MCnvLAUJHQ&*;||w zSoPd@ykzZM<{nI-TItrU+P6%>#odeAsKKHllIBuGyv8ISREq?kokM!ts;)0DEqudx zs-i2hzTy__RBlEYH+@icj*Q86E;x{sTYdWq>fLRZ+6Dfi`b2`j%tP-jS}`{rxhIhQ zF!ZTSUiQfTVKgru;$;)~N(u$wdQ8WW4KpG$96w|1L7$+X(q zpAhZ13mxB>A4D)WAB=9~{ysk=UQHNq7PcO~>i*I)qj3JcP)r#{^rM7OX zK7$4s(?i(#&*-)YL#YbsRI3WVL&SAV-f>6cP$CY|;5VIbXx-}+wWY$-c$xQaekh4d z8d!KQ=dl5si_RH8*M%RxhA+1xxng3_4xI_->_^_1Pp_qv}Aq2 z?i+ahr1}kP8^=s(4-`bX#a4k%LCB$L-feiv_MZrNte4v4I@Rge_HfC~@3n`DS!Xe| z?7CRAf6H>Bn#ne2f`Ur3_aB2dN^pdcz8LNfPY>O>u2SE?*$&A^)gf`){FEQ`z9lTl zoG~1|f!D^vlXx5Bhm_NaZn$kuA-P$gbT}X+=Wqp~FWb+77ZjWtT+zj(g$i!XRcFef zi#JYlMQi$z08pDOI@!|a*3_uuP@%bCN$0vQl|y-((XI|O`1$bDV>+*lPL(<`4-0Xs z9)wZ5qn;ct`5-NTU7ZXsdpK!${OrsU02|Kp_|h0JVM+%TJX!nZ=e^#~c(F|aFhS{n zFa0e(wa>JVL%_}-YWxSO)`1-Y*1U#UdXwaYl&ub}AqxptHXNAf*~47I!Ob%j0dt0p zksX(B`(RtUA3VeNls=t1x9o~0Q$?Dt*(e4~9As6p`}(@;W|oj|Bz^q{_GV5VKiQns zKQvRZWZataK?vS9SzeHpvfj|^+^)8+=CB2149HVQLKlZK;YwZW@%I?0g!9TNi&u8( z*El=_vJ+)K7z^seEtKdBL&B)lRc*W3(w?`2u4=3yPj-d2tJ4Dq+#7VMLu$;`a*l3& z#BG4%)1ctGGi9q9tJj3J{HMc9ym171thIo?l-0mdHS0PzJ!^ZHWJ>nz&9A5I;)s}Q ziYi1<9am!3S63m370s66A~6R`+9kCPa9Mk`mM-)w2o6YpRi3Un;Z{MK3XbX9ax8~K z612kh`Wi+CQ)-YOIHfC*w?>bjBV0#ZiQt^8_GnPk+z$l#W9!+$Gc@-h*v&3uQ`ZVd zx9)wQXyNl?c?*m3J9fpEDwQ8j6Yia~HSBZ{6SYW8k-Q_FxR@-pD`#na*cX!Q8ew zA1r{iElB9Ts(;!Hgr9q;WO8kJK04;yy4eO{2tl|n0E^TX_Ai+;Lkf)3re=EGfOdd? ztQ!S|H7T)#&Mm}OqaMocW#?r0uCWZ)V{JsdM1sq{4?9JUDOke06sPTQXSEz@eG&CL zF|*c-PZh~Mj$|CZF`{K5$A+q0x0)f%$$uz##EyPgWNjX~jJu!GA|@(obxzuGi!4gW zD~LgGz^8X62Amr5TAX=>&HPGpv&`i#=(8P6kaso6Rqzk=_I^GxEQX5dBZGnCq19W- z@zkg$A|)K&=$n);%U{mlt)|7#Wg>vySQH=mu7J*~s{Fk8o{1FmOn;qpnnJMF=}l#xDW_L~h+XCtE&`cGu)Ch?~vx$;!cRq2qTy*c{FKnv*aYq2dYdNC@# zGYKw_y5QOt`{P!!=Q zIHFirA&QfZWgRIk2-1xdP{_x7eLa+eb1An&W52fH9PnHK&~p6_7QZxfWQ079!V?$l z>Q>kM#%;s9g#_F*{4hP}{a8ojku@Q5tTYxkv_57prE5D;Kqo3JM8Sep#)f=)PGc#! zBJ5l>mJ*{_bD)d_A2_!~VAC-JDdSIRu63G<{GAcBkt4W2##s)AuLNHY8h_@oN`@Wo zt~{ca{U!5o>k-k7?sEM{cIbeCJ4fLI0baRf{qO7^TQ9>nh329k!z8?^*8X8*51H?jl~&MlZA6$7x}9F?Ji$7HX;C)^RqKKrd&{s zIDBt-MsZ@LAff=}ZUvH(?((DV{T}2GHgVag8j_QhP@XL|6L@ta+DG1M_cNnY)z^ zzg}Hnr2~Y?Y-E*s-2w%eUKz29pnUX=H#YG8G9Pz>s-n;vl^`&Q+OM#5i^HVQj!`6;dmiO+RV6*>+)cN z7p~REI5di%E12?u*c6Xm?5Z0S=_V=A$3qn-_7>zMCuQ;kt%;NPm#fY&PXm3)Kb>2w zXP+!pG5=~uI}w)5qT06w%l?{UfQAaPzpVpXJR%1Fa8W_MpX&l*a@>aP0E~v+@WaC0 zPLST3kR#%>lAB}huC}9M75J_h$>Xecu4$f6C zW~8IJ!fOeSs|LvfOEG>c!K@s7zQM#F&u-GGq{-UMYfPA-eGcM1R$i|5k2?B;x>gQ7 z9&(?3-&sJL>KQl549QEy!~N$9+N{R}9;2y){tI)pVx-P^V^6RbfGgZDZmDN335a5iBZm$e?!VkAoSNghIt43Mr zgd-kp1@r9-f;u|Tf z&-0rmq}lp%;@M*c@-`+#=4)!`3Nh;idVciyJ15DAjXV`6t7PPleHVvr_PTQNaP6yp zvP17mNC$U3HDiu)PYkt4UB!H{nIb=e*De7tAYK-l!;QqV-tDG!%A01Qhn#o68d!Tf zG!xC|#nM}q==Fgo3q$0ZoT|R+naRnAmTr~uV1<*>nq|<+C0Sd{SE3myb-6v(gp(~i zKmnkF+hEUHqUWh?Z`(VU#<4*FD)iau>Mrrdfc6%=+~$8)X{7A_o)E18cVAol zRQmYbNVvx3$t1_$y1x>ylFllAXue9Wa3$?6TA#|{>}^I2A^zB)Z7fmL zfeRST(L+NOB;)Y*4CE6^rwO@!8J-o8e zY&v%s=ThtgpkTFP*Pb}XSZ)=~B zr8^tFw#1qmN7CoD;YxzlEu$iq+dmIc1i8wGd@W~3@8xGX$(7@2$BGCf9Pk>@fgSGB z^53R*3}4_>J0#tgMxV5%+`D2|4q))GWjMEk(k+<}2mQx|;#It73$9bPNH}SvgBGiJ zufI&RG%5_of2@w7UCk#aS92c~5Q*8$5roIE!9!%Vx4Yt--kRK~9@er;dYFX$6vAw)Og@ z7zc2w&G&kFd^ata9hAD6VVuRM8`9y^V|DnTnBGkMY@Z`4sZf$x37s>)PD%!vFd}vtWHAcZRq}tbd%N9m_OlM~ss`aas@~g)u zhh}R`yHn0cwW)iC*CYvDLtN06UC+G7neR-NHotnNpt{=4X0}Pmk*@07UqpAbEV*^f za6-<&wa@#1bCp15Q*&--77JWELKgK3yX=w;1nAy_* zo9LDH7w0j=)Q#HJ{@wIvXSFv}AkPi9(Y`=6v5T&_#=AwOF(mc`6p8YH)^myiEYebh zZx!L0t`vv(Y$dLz=rZ{&eX>rBIy|&G)JW#;4trr}8quj<;$u52t>17DM3G5>R=5GrJmma6VoV<0 zJ}0=~(r-P?yw-gr7GJW3p< zIMM1c+GR=}yxwgt5;H|I{Hq@>7c&*&8L29Zr{C+JkDSA<+Gj+JacKGjGI10D^f;vc}+r)xB zwXM9D(p&ty=JoMltK34Eg6j~X{tG%;pl66`MD(3)J}XclOsCLz?`EKEhJ>)R_56NA z&+Euh6X{2UeJ`FkCmnHh@n^z#qw4SGdDoz`&vnw4ad0{^D#@+Re^LuYSEIwnn@lXZ z6_}k?d?R?{3HjB;N08!ot8((S0)~-^#iz^p>uKGz0NjiKqM;pJ5qlfO^>d#?b)Dy^23weCdswy>nddrOg~#xlY8Ue$t>jN z2+s_*Aw~xgZhRuWj_I4ce`GfsYtChz6u$*60W3bp!^DI=vQck{!A+&2#5tr z?mM@-mntkC;P^J0(r7;Z0do2@VX>#?3`Vc7XI%bhv|p)%xJsxy0P|lGpr{1`=z9SA zPV6HVl@xzr!qAjcclMx?gB_9s`t{dUtoq_JZ&L}4)EN35UZt%Xc=$K#A!^o^S7g`2 z1wT{Wpi3?L7C(3p8S{Mx03s9}mT zLg5l#8@a{7hec=;*!Vt_@9P^2BN1&8#wO+&c*3MeBpv>Zmr#a-#gukiI>1A40RW8OQ1G zXtg-5-ZKO61N^h(gN83}fwQ3TasjmMwkRDR)K?Xuh^V-0yi0CfL98Rl{W8&hEAL_h zMkSc9APG(0e|28$wpmZtnTvs?l4I6Wlwed4d0|V|~Mt}-9WZH0YmKV}!g($5JHu+r0D3kK>CHh86?zjL~q?%QQw zRL>${M5 zs9k;3`x#DqzOS&+(^wzoC zmwmfHQ$uHQ{NFkE^I=9x#{V>Bw>p$uLE`-|dl$&zJQBs~?Lq~3kJ{DG0?J3kPY5o@ zpJGJ^whbs&zt6sC$g13v#D`v;ID3y|e}*&qjOhPI+k1vJxiw*cp5QJ~kCHq}BI8Tg$zuewRaPLUpt>H{+CN8Ji68>HkG+A$6K8Qw9zMafeHR5}$>k zkt4?bB!FExWK!4WGpeuH4D?Cm6io}bM!ySflHNf-cUh^ug%165**MUC%pNfLESG0P z@Hh_5m9||c-+Qb`P2O?qU7iOn@}bh^!X~`^H=uJBXzFKFv<9D*nhUtSE+`ONcH-Qj z0axdM#fRLv`4S@nZJ6=r%ppL#QmvWFHb6JnDpMC*Z@tn#FP5OQ|qZR=v3_g<5` zqj2JeST)vu8poCHGZHp2=!e9S)J&q($HUzxGTlmDobzX8`_gKU@d4M9$@pOX;6b=< zgo}cpR33Gz9CoB%P$OZum7Ny5#m8b*ZCNxuOd$X)QyiVB^8ZCv$GIfdDRa%14Xc0*wMl?`;xl7LlSs(se%V2?+z5FZm1n zeva%%hr5q9NqVk4)toO=nob}}R0QSoBWo9P+*gj6F8Z6NGyS-JB&CXfd2%wZ5xCNu z55(2QE!Ta*YfivLTuS+!VXvXq`O^#&GLc)gP=6+I^kkFwc%IGWb$ykP+m*|$FC~7I zU4!y%bWVB*ta%!E32eF`s6DZ+UD+XZrDFMW&^lF)mf)bXFRgAZN7`f~b8;3@%D<^O zx8xR3QGf9hE}`M0)!NJ=ZkCUNk}u8ogO51v%@4%sIDy^ zOFSbO1W?az^&nf~9U9>anY1=sU)VJliN_kj$nlneKAYUl)<{Fwe1|(gpj5pbUI)8* zjMh84C!k~zRWG>y(|=u{9(eu02zFLkZQL75^$&QR7AsQBC8#v=>{?=P*9}eA`*;_F z6&%~ha_J`eS?*aF=HuRGhv>E0+y11g)pH>DI<+1h$p%}_c3+QF>?6&(jlS-$moiY= zD0H{ydJ%P5D{=J!u!v0`-TYjFumwYKG|nH`#1A~Mb-VX-eJ0jF&|4deke{ZoK}YK~ z=H`4h4?lIl#~za()=gzl-p`zT0q{c}w7e9)Ar?27_xQm^Lt`1WD=3`EMLpZ76j5Xm^?H1WrL$1Ua2+F*aMEiAv|06@O|Ek+nk+i3TAuG`NttD6?oxZlQeq8`;C(h5eVhJ zPAIPE48I5TaE`pl<_iLxF3+!?E^JbkwMqtSozPL!dKE?Tdrt#WX6tIFDw#)c?fc=f zW^qU~r}M@dw?ku4;7n{=Y69fgU(sTO4UN26>W=A9>?ak1*VfuGHvJZROYa3Nb~TTT z8Py4jrY~Zr?Aj!#Lk=0s8&m^x`=G(OuNT=GqYYJ0)=`565AarZ0(rr2Zg^O^+qpW) zJ`fZeI8+hK2ti1C2;O62a?&XSK*`TABA!okc@*YUWfwGy^Zc~@L4&VqW`VGD*HCTl z(l8XiKKDJCtr2*=ZnAM&WTb+qVN`)p-mMK0hiUOuf4_~jiB2&tBvC!lSgJF8_x|)q zh5kG8mX(yjvX8HpY!*5@uU)G@K{b2-zytjXxju&v(Jp z-xcW0FHoba-!qe{O1 zmQ&4SZpr~A{^=Kys{h=LiJ9f>R;BxpF?+78fpJKwG6mry2dW-%^Dt5HA?I0kKj;lE zb|fV+xRnAb%-$-iUj?zUR!pT9ozBZEkAb{uBV2O=*by|<3b8zs)wDlzm+z%j^p0S_9a zcg~V&sR9aI{FW<9{bZt62*Z_wi}fejrXmz)-dkccpYHulAJ*F@^d?YMmdcAMO|Vq*%`5P5t4FLQ2l$Oe!m+(;{pnHp7gDG(Ah5QNg17Cz$@&okV*gE=93AeUX@b4~oFEQ2SB+A&4bPn@ zG+=Q3r9V1it3Yz;b#eXqZ%`Hu*f_sC?qD;3D8WBva$x`yNE2T^>d~nWDi409%|`sv4_rF z*3Ef&=JH;D)y}o)S|!3`+DOZj@V%(N@%|Po^-V4h z|GiqGv_x~XRE?%Vy_cmL{Jq1oiUnmgAZ+-??VteV`5DsGZI*|9Cs4(~Mho+m(s}_s$9OKO_WW0eTMSoRU9q~~p zK}{DdkIpoY%nxPU{_#%c$z$aGGW!`>&*E?33I&tbrc#Ux?ART(=}{kBBL*eA+Rel1 zPPkEYqdv7Wn!YAjGrJ$_(NYmQ8G#zev*nitjuc)GFIT1k-D{Da% zHZ7hhZ0h4HJxi2rFYrcCj){XXgTj? ztQ=;RqJXVnRxHcEbO&WVdYxWdugJpp;GuUIV-X)}6$2HoCZALo!wx8m{IlC#{I$CC z-$rJ`n;Dlm(;EQ;djEX)d6!5KuNUKX@PKlD)5+^EX|zh|wfN4JJwR{IHvk**ra|Jh zeE{j=S1Y3m>bS}8l*P_t0bC0v{E&QqJ?|eB@hDCVC^aZp0~;_Xrl5~-*K4aFMK8;y zDMGe!V&YH+k})%gh5fe1)+$-hDTdYzt4iny+2RBPtyTZXxp{TB^{%M0f9|LCr)U#f zgtLQ`#YLb8h+Q%80ERL7!?#`qd`x#?iCze`J^bsdJf|be#ZH*@7{?kdyo@{l8Nn&q4Z!drXgn zWXs+o|K?=Xy1ScxsWwssp!t4h_K%wkGG<$f+D$)x=tSyA-He$nKpC(ay~yt`3jG2P zr|a(&eVX1mPG$+H;0Zq}{2K-b3;UNpi(8We>&_=)J~)Q{0~-GVs@CDHv{oIxKJ|B9 zfPsAuRH8o&&eHwO(Zl@wFVfyMVStIHaDDJMhEwRpQ~I8piav9Bf#=BYr+$TQ_XpL1 zTDpecaiz3&(R=g)if7tun((^~ySRmKlF5YzGMOw4l(@x3R%YLk((zW61iI1xUqBH! zVuc-l3&XE0e>b=94qrAF{GCJxw%Qc9S^`|kcZ^xEfP)BFaeBY3Zx44B{dOsw0jpPa zw2wh*ZwnEX^x|-!;q^OuO*vPPm72g`sQ7QdXvNYoEwaZb;@nExYuax>@LTTtUjX-` z({7BibN*7lN5id~K#%D^+XkQ%;lJ^b0sn3S$XV(14`F|&(V|;`O}<8edj7dnKLBma zHNK~w9#o3bgzDVTdh?v~yfQlzllBY>`Z4OrVQqHp-Hng$H9pSDFTJ<-5)*1epM=E8Fvi@M;esz1GL+mnHQ$BddP8Oku|MjEb z$Q2WwX(YY`>`}lu<==e(sU&?hx_~-r%os-w!h}vuo{O_~X%8PT=Xc zZ{0ejtgKuqr1wcB_sZX&^$#QY>}=V`;{Ei=lPAm}=@Rn)^6593TID-0n6u|v6K)Cw9L)!afz4_Ckhp-w`LH3$U!sef##M<~~s=ZChAaINA2WlR03&1Zbgj*4ylN z6MKC8Yd)mvF6Y^c*OJ;FzL2^sKbCcch!GOfBMxTXXDsn6L%_ob028ocW=xqFKN-=d zvMg<&!}p26-@j&XxG+{c`za>i5a=@j<|?Tm$zlY8n)#mH*-%sjF*wRd`9hz7p1ww) zk1YpSs>YNf&L2e%x({g^sHkjkok|W!%f6hu4uj9W5=H;axo@Ruulo zhcdLORQbz{Hp!QOt)+FB+jT)w*&-shdZV1|$Q)v3N=TJqB%Br(3p;ym^~=~J?1&r@ zAunN{B6i>@`>gHq7;28?*4rI`d$R(3ryJPk(~>UHK+vno7#()jY#Jl~&|{L>%q%SOZ&j3e8EfYleUg?e08lhS z(TKl#0W6I>vhHJN7h(A_&RFg@nOjl_15%tj)ZWo?q)AB6b=?lz#llii(yDCDXwXLz z?7)t66&4j2^3v7Unz;7!_Hh6PM;clU4l^`a`M2nT&Wp+m*&YBkoP~)1uzuo&X|asg z?*aWACNhAgP|D_yfe~1DmMb$_xg9&5PSX*5?W&2^M5RD`i#0IJ{{oXO3qXenxqO}u z6EjlkSw@3th3==r;`4vs30KSD2Lo^ZXEZ@6f6{`@0M<#nRB@Bh4V$CH?j|Bo;Ol&Vs^*VBV1 z_0D+6{B3;>4pvu%bDz_6>pbS`G;+zo40tg(Axl^KpuhTj%lk4LLcdj@JC~JLi}1|~ z6Un4GLV_G2QZdyIMYBBZJ>45Q96@AxllCz1xY~nRR5^rXKKZpQ`Z(sO6 zLcUE@+@Um;T*DuyS+^nV1%YmaP&;1ovOBj--aA{hl;EvXIP>tj+XdWN+h?N!zQJMkSoD*FTClO0AUNoLOd-gDjqQ*oe82 z{l-03gks-*@`cBvIB}CN^Mtmv(du?|!c=cz!s&~^5$uKKF+UbCVeK(V*ri((duad0 z2F-0{vRN=K>#TFnm8ss+j6;q4#4o;WO}x5vbWh?V!)yLQIaA436-fN>198!st6!C8 z`ap{DI?WN3=j_wS^0VS@d#|SRs!d#5u^;a*C&YV?6*y#EoyQGWWbMSmR6!-jls*h} z)yIo9DfG+150+m}*6Z?J7hDhbVk?jJoWXm!pBs6! z_g=a~{epiR@EXz?l)$V=PwGI0-PeaBDaJZUufMvwvrp&d!Hw+Muatez6A0N$_l@4XG{H`WQyvZJN!!h7md-^$tFMCI1S-}L|7 zjk791*x&T;8>!3}^ypCNw3dGVF5?(xNvV{jtWI3FVjl8hjCW_lsbCRbC?NM9JLopB zYyAQEd$G?ZZA|IjDYqzsXNjp(ER6LQL@Ph1-uZ)8k~$4Uyws#lO}%f8U=L3Xv|2)@ z!py1f8uto#y*u#UjvQ`@)5;^QTY;zZAEn~$>)+#@6II7Q@f8} zxoXTSUtnppo_=L%?%ICTP4A93tF=o$)BUB~>YGOuoJ06E`k;1BCf2G~M>qg^@0olk z0p1z6BhKQ)Hm~jKAHezf#~U>El(aMKEYNb6D2P@!NcdBK}Ug@0_;iRW|>RYBl4} z&&y4xBz2Csqcx>L_sgEh3r^=cj8W8dgp9XOW63hVX!I4A9{{z2qJ?P4@NIltj+yq5 zsT}b30FjGjBb4)6x818}c(-Ba$}PaE-1u1TYN&PloOEA_?ISPX<&fwU(Ovq`5EXCr z4QAz1^lsr>0=2V&zU#d$o%3l>vV^>S0HGbf`SoHs2{QDgcoSgNeYx6v1RB?8|Fqhp zA9b>}|D>B=U$LoSrRq&o&C)08{x_@kDatg}pWiRq7G4JV4mBpP1b*DPvK-82*D}&L z54^~@Eh^`FWC9RpFOD%Dj$;(TW3T9g1>zuddtm z=V1e2WW2|h!%zJ`+V}8ln=NA+1PP=wMyaMM&;e}~=;R-E#Ut8a<+ryAPVhsgZr!s$ z9bO|;I?eYkPL_MM-0X7a=Tmf&P_1+or3t(X>M5J|LX!i%H*7Ieo^6uwo`F9wZ^AeT za6)~O`ziQ2iy?2xIlrRal#Sk5w1SK6YEOY2DZId5qTa51Jwo1g7Dn|Bye>;_Da#st zN`Qlf_X-3&=&{&XZIhlKcPUjZ<@asx>aH}CcSAw(eju)mpTWGkrRk{;=3C003D;I2 zeG*2d6}DZa^(sp!Sjst%hXYVc%XD~JMpD0My;wN6q@1?~IpFp zv=crvSYD8nTMI7i?~w4st}PKWn##f|UGWB#0k``6%o>Z_rKYiUDlT@nCoyd0@bjJ` zGoxzPnl!ILC(+qN%4$wW(0cEIyhA1>ca9$&pMEoo;vcN`9tYir7a#Lm{?S>xu7lQ{ z1~(;Gsq`n-_1znC&FQ5S+m*s7XL@@SmIFUhvzGGvlPwMR@_Xdp$7I#OPM>k({R9~q zy=vFzl9k?NUl`!^qBVH;~^uaf--G5y|Zm@_(Z)pcFxbnOj|%2w=mD6y(h$+GJx;&(AdQ- zZ7y-Joj48|-|OiY3AV3m9@Y0dWVZq?obY>QY69!n=oCjo-&&xjvNt>4WYnzU)%Uk6 z03OUq{p548DC$suje)MXVP*fxT@Aa~lrM25&Q{Ob2Z=5aJL7^j*G-e%hAu#cY}l-t zzslah%i`AF`eqYx;QPW<91*dHnMJI)rB{J5Z8&3?^IEp2`zwO7V2V75;pwF?%)?SLxa(C zO-emnwbWtXyjp6fDn$gh77yx+!NF)siXo^cd=4{mYI4q%!#<>{a-X@ZFTHx-Zs?S} zUlp6IT<8?G46o91E20kgRMfd5DRMLENSk6Y=&;-^jg5Sz_L`OIA-lZJl&A{(rK2^g z^KGA+0xF?ziWQ$?VbL6718v?KYipO2u9saM6YKPZ8w|nmH^BA&K0V%9l9kjTIBV42 z6tKMlvsYdnw}pfbzkdWrEku3(io@K;rvf4gmKt9zglFc~q4>BT(BU5vd@A=7y=Hn^ z{IRGr#fn6l6(s4-y%M-%Y+Z@PfV0mC^r+Xs4r!ij9(WMkQ^;SmSfA>Ki^em=1Bx{M(=uTAl%tO89e&w*M z_Cbx(TF8TMx<_X$6=Wz1xw-4BRws&w?h96@(Z(YCEo1kRQT;IUk-6Iat5^CtqNwo+ zc@elKyTpPbKm8v0zB7Z08>Pq8Zi^&?Un|^tkoN_lUwO=~*i`bVMUzj)&?cYk1zZ4X zxp@bNs3D~R@r9Z|GrQ5}a`|GUYAEjL0<0KsMO9yWF)J9znfPw5GSF&4eJ^`Pi&eZ#piGqb@!7NRcQAeQq(EKMtkrN0DFV(*hqzauKWqrY_>5|R$ZLS ztG!HiC<~OvS=UKR$!A1Uh;{ys-iX!J<5`-+&(6gWk;Pfl$IX>Vc4boymO4ZmSl-}L z5$bIFnqO$GoYT#*@y-P-1pjGeHmai?8ek?u<5?Bx5j4~fTp{Emm8KGHa?slA_cmS- zs!T=NzHDLzy+IZ}t4R(mVR`8%jzWfFE%s<;7<&@iV~(3z@h_=#ug4RSU!~XV{*W4J3y>YRyDrj98tADyM7WaT(J}|XE`7-FP zCA7B}rw@kTH!d`hZ5#hG)f?n96zTIlsQb0`MPd!DSwoZ7R8B+Cc&6=hX?{gI{+s3y zxSF$BmASDpp%SA%U4e={%I<77Y~zaZw!!SQ0hu|Oa}1tuk+Oiq@rC04DEuZIAMHEB zYG!MWT3>A|wbUy5VcC(~X!FQheu<-acP8i%HnW=9>Av|PDGS7W<6STXnND)j_eYgxGw^C54p|Q%rUMV$QIg%%; zrWK*jB`)@lMkiGGQ5F$1J$$C{;RnG`j@cqlzvQolNThE;G-Q9OCwf-0Ipfwb=0U!x zgAS-41x8Se%0h%dW3ley_-q;+H(n*t<2T`8ejh>T7n)pQMSsEAWO!BgF7%r`hdgx3 z$bi`l418GS0Ki{$t;fuN6VY&3M?hG;bD9v>h5a%cd0$hK1q++f?+8Am-s7_J8ry)g zH9v%SG{paDHXfo{4K<}MIX*qXrZ)fVtXo&sX9%TcBHYhnlJ@3eUK9>tm3UXLln;7z z<(rLle@@^XTQkSoQqz}mxEG<1;-&;B6+KZ?%)o`uJDh~JR z7G!d~em6*C`Ajyvg4zD2acRrXUl_MAbX&HPd!Q92XmBd~7@^n{8Bz9lucW^jh<)mu z%1VvB#`=U!4Jke)Z7G^G*fH6JAx+%`y~cX>Tg*RV?e<1ARGJUYKHQbM(!CKd-=*V0 zlY}c3YP-nJ21X%hT~y4&^TES;oukEyxKkOh)i8NCxJ(4mKB#jrW0c}>qe$>X2>-C( z%V#m`X{E>+2ZXTZT7Gsddc-Pl!wf9of#Uvb88DeMzEJ#rcTGvg#MTaom&S1xN}Yha zmp=JovI0>@0!w{ zI2j)1NDmxb$=iQ+IsDi~zaLcm)2i;-3) z#QDSDi=$cXhgJBcNsYwx1W^J19y5XzrsHl3KjguXj&RCZ>Zep`<5vfODMppd4i;;O2=kpek5WCyRgk;SEbuNv1xo) zQ*ZWK@f~SepWKN&z^_U=1s`_z50m?**n0L2M&`@xo8o)}#ZkiKo8sMGsZC!dLqGPn zCSdZE!%0F{n}-pdUM_EA)A2C2$i)UDhs>4i^B+1yI8*GKqA-)kIQkr+R%F|)odLtb zBE01>F!jD{R1oP9X^_wD^pCuuRa6W~QAq8RS6ALB-(>@cg_VzcP?!6GHn}*_E6->C z=u^iTWjBA3&sE4bFtt+r>~wXq?%a=pJk(u9ljXco*|h%AW2MISwjao3@xJT~A#IS+ zK^)Ir$Nk?6OOTyNru(z~hZJV`KB@@V&!!WV3Z2<8w1zs0edij;ta*Q-J*%XT@mA@al=`VhhPbZ`U9T+3zT-7 z%wF1z?S3q??@;(l(HBxondx0CB8gwYpUx)wd9^zdPc210c=y2|nN9qHLgZHqfFiWNN_U{6g2wo1DtGWK?sP=`B;J4V!Db9flAI;v+MXTH2 zunj+iJYbqMx0i~8TKOBfZL}L1c!LcZ_pQ_($!`BPx_^-AbqQiMT63)3$k4m`d9C%@ z>zYUFtA(*?{h`V3D=Os5Sb4z}BGtIQZGaD?a5)}L_gg&1G+rKN$K5sWqhA?gkdSoR z7BnMfR{PZRVX+%N7|Dqmls{5>#ZSWJg>Aj&2{suba@O_*ulS9;^jy0cR=(hSFmiRL z7^$h0-{~ z-OQ*xw{qku;RNRS`p@ihRhHqEV$+^9+Jvus3214mH_f6}W5w9_=LRj+*HvMndY_;* z&Sp^moxx!1%9MD?gK^UAH4ApMWFPT89&Jo~csS?}Ssrwid7ftl3cpLpvf^cfoP2+A{i*Y982 zjF^NPW9&O>ZDjugW>*22U8y=^X(D>+z>^EweituiOlYE*Ml|}H*Vfj47W;&1@iKRx z5ek^ee2N0^#=18LZcK{SOg!G7Dqrpf9{iYD-N2_ZbI5_;|NaNZAFp89B;cZHhm4oZ zb{mmWz&Z5~&|a(V}7LZ8C#1O;>WOhuZbtfvhE+hiunnL4FOC*2;`(88!n zOABaysCB<;;ic+;3ZzhEZ4At*K5#yGa$GkrY@B-m@Q}U7v)9Y|VO?GXb2}T;63U8p z;V!6E8Q30C+U!?J-F(N}TzVCgd&6|e|KyOH*79?wI4;+2DOp1`%59?X!LqaMVr{qj zgwB|^-KP48n-slB=L0FH2RK9XQZB+ez-EP`SxZw!Yv~^F3PFb?u0XaF_YSY49+-~u zX)>jg&&BEPKZc{dLF#?{pp16u6OM0Uhe76wxq&oV7XY z9RBuFZog%c#K}$5;rZEIkeh;}4b&Z?*yDAk$Ob$W?EsdMEHEGs_L_k=m9&u4u+Bp z*hde5bTyGHs2sd1(`>10dKB3#?hF#`P^M9MbIF3Icv#}%_F#uDP)ATc#-S+% zG*J(o$>eRGhE;3Es-;;ug8C974h?lu&y|!u ziM(2coGW)LnQQ_VHhyuf;^g7SEeHpy{nALQb@?0`NrTh;P{~e0YCC6dwiZ{)qT46 z7e&#h`uqUNwcfz_ErFVz9}B*|8LZ#DZ*6fjTK7TqKxQ!pTNBs(`G~X_VB2B!!J*7+i$q0`sdtUV>&N1Z=7^c%M=dtODa5@nLhr_h6l?>vtj{ z24OrkOxCsNd+ND-ayolL$V*A|i%jpuv*P|{MTmF(T`RZo1rYV=Zxzk_cu5bV*&6J~ zSrgM9Tx(whEW#&)gK|3xqU9Kky-%v{89?fn?kyhTgnKL+h2Da3$D2Pe*kV`L5i=?_sX~$Ns#4W+Jvb4C z;w}squLlk}~5=HYsNbJ}@{)Z`acIZiF_N8tE3 zdA#*k^BisX1dwHXId_X? zbp)1V2X}^47#-r4D=J(TV85bpvk@C-P$!Y}f}} zrs1$2STfjLMr?C=<)}4bnGqHMpY*+TN&TBf*SJlu_86k9&WPj})sHZwIs}Fr1Z}D79Wp{}AEmclF8}l6y)m2C0fkSE&cZv4(FcpfD4^|)kyx1Hb4{Gl)P1}<~ zR-k@!k#@jsj8ZRyiXMI?``rSa?JF{F!@=5Diun{Nh)WBgJwsVVk}Oq15O$B3yq@o# zI-RG?hfB9)vqgHf^17~R!2Qo1w5=l8)=rKe?{Djo)_|InfljQdYf?jt)vIl5M{7z~ zu)zDo1|Z_SnV&+hHXEc3UkuAg=|1mx-f{1z!JDKWOgMw=YGKE;1IGuaSy*D7E-u=vwh`l@7t@vk&?Ggnyykaf^0WxedVY~<#E5YhROJT z3(@i;F;Y~c%Yc_EUz$V*$dVS=XBLyedU==)N}~t>p%J zbnAqu0JK%@gIAd^wF%rkETwbOuxVYPeh(9=T}dz2GIgTp-d+>Rxj?td#(gXqyov_t zN9CIa2o@(VUrDKUC1GjFR8Z(8Ah;6YU(tgK3mDMmR<=1XwIDm%eAUv@@^%t7JZ*W1bv45hmFg`fCe#xz*39o7;u&Fq zm=RMvfND&NUbBJnr4%q_LX`a*WSf=paJ~sr9@)I5MJGfw&p-FKn4YupBUSy;e>aPe ztOzY}^g#`VoK#T9zqawUIEr6&g)n0BJcj0> znSf00TNXDh;pC?pUMvH0gR_ww(*oj_*rwI=YxY_b@fQVkBo#&#n{_6X=v0mBxzdh z%qKxjI~JPM*-=5~+vt#Og*vtfpbA39JB!`(isyVugdOh2d##5Ox_tC{7= zNG+;8nTG>-V`k&Yyu8cNF%EiPb7GNoQB*G=OL)T6JG~h($y?(AltL_P zD)pPIAriux1-vPq0|e7A=bsAv;hiCFTJ(bM{~XHIb{G}Wul3%LmE5xgTmH1R+d~gt za1GQFF8Pn@Sn3 zF&SOt*14&3ebiTY^a&~t#pc9i@CVaDY9=i^@k`&QmSr%;v*4NEyU45#(bY8P{tEIY zP+`%aVT85WSn9XxjD9}6N$$Qr;I`j`#zSCb@z(2*?+8k8yk0aL4MayjPsd{=Gidn& zRnD%*U5DTc=4?|gfoF08ZxEr0`6s~3;I z05fIc0%igmSS{~n=Y6g9-A}Z?QHfchq_~Yui9c_)_OOap1RE5kq?4=3HgqH&vh1Pl zTp=W6q!|MVmT$amXly5}vUxN7*fiRW7r2IQR0q_g^5v+O(R4IU4OI_!N|sRHH-yNM z6O9o+DM<253crd^>2$;%#lgrl+!xH`_z*ZDO~nmy9Q^dspa%iBS0_0oCNj-?3EhL% zOi0n^en-9)!}LQRI4>}{Ks!P2wJ_0Z=mj(t;nzEl2SQi#y$Kj>))hPEklDJW>S53R zGy~q%WUh1|tyN?y0Br(b>BICRPY@;n$9hy$=<;LSp0WD4XnVyJD*lv~%`5@IsVVLf z3K2h5of00Q{L@E^>dg`e3kb@^ze_!M$j{K*)%_*GVve)8_xJk)R zgGvb|Yp4E*^*W4;IdVtjeVFUsS9=BSBiaSdei0AHC&i4T<_E`GtP6EQl`wUQVu}ZA zr0%DEc`*!=-Lic1G_x^*PcXfn(*Tv77w<~0IfWD$UT^Z#Tck(T)kCZE>j&?g>^&JM zldU<^^;18u+x)1CxLMGQ%_~u%9*deE)txT;Z?D%y%@96pxWBCHuY|Ke`Rna|E?B&` zox4|4X{AX-UJw3EyBu|ZCe=e-5YOeK_#K>TbH^fDxT@MHHb>QUM7H@uK$!**NPKBP zc3%uk*a*70vtbNT4rh7fwV$QR2jE--4Tf<3mRGTJ3$?rZBCr;(kUrnksaJqZ_Hdqc zbQ#z$wAeQNGfXw`0x#2eo0g43X%zc+pXWZ~1(S17? zTSPCnc-q&>rBw=dCaHJEljaxff+rt)QHtCM#ftb>*BuKM5Yvqz(}f)7eIy1v+BER6YFfYlo_|QPtNNRep6?pU({_=c@mQL zPLO2tN>QlCBxDfb)~6_+X5qqXsI+41bkF>S3$z9wD6cD`v6%|}R?#_mCNH%Q>raLs zK%7si+usqiIl+@oelz=MKU&p_OWJZwqFYcfZ3qdc$whB>)dpFCYFmrAWvLe%LDzR) zz~3plE%3Lx!^S^uh0QxuBw2G2^Tf0eWdt zE2U%Hp`-M(cRgh>o3(Y7N^+5Yt`daX&3(nxlcO_zE&jL}(+_%yph{=D>XK~zbIsNF zqr-VO1QkwuAWudj9-H~R;>DEAHj(e1><{ZJ$5;}@JY#9HnCCZwo1PzFSxVsQw}M4- z^9~D6teH$#-g0!F=P^7w8{$VjFq`oz5q4(!)h8l zSBVz6q!V0~0!A87DLS&d#3i;_1;2*2MY(B^?r-#V0COiE++>xZADKIMxcdK69KjCMU!v1E)WYChFFR2lJnj-OD%O}c@!R&QCIiK6T}i0$0Cai#XP=3mB+vv@6xNBE>rkDTx- zHkWnnGnEbmR333Jc&K#O|K1gA9F&nD()map(8u-)_=g%h9iRM)hmW zxx1}bNZ-m2?40CbzYGaqX=sy9;8E?=Xr$LGvEhO;Dz1PmEL;(B5ekiTKQGXUDYK+> zR5(h}CQW~RWZ_Fk+T4%<8K+(}tq&Cj?&LseYw@T-nfWWVrJ0J{rEl?I_j?+F%Nw-$ zcFgFt;e5<@?I2k^=?w>}1eHay1=2hzek&PHX)?|JHRsp&#prQGLH;?dW=;rl=>GyX~deAcY`t;+U6sr&A17|(xnVKC!xYBKSa5VH!tPqPw zZ4AyHUwsoM9nT_vmZe_{w5bizd^@z7h6|!v@Gg8CwbV7jmhK_N#8&?dz(pKf39d0Y zA<93xV2e?ya;mV8kyb`4Ecf|&BWd2I-8Pd4hj1|I2-VPgPIf)ho1H^oPe`F+R+YB7 zS>CO_!g$HUz`frjK7~xy_~VSui6we)Tu*8J`tAK*l19)4Gt7Fs{3T?lgqv1A?F=co z3v80Xr)i1vSy!-7zu#Trx1^d`^ilVlL3=F7t@f%0ZIQ#FteUx>>qks1zkL(!{FPcn zOxxP7Xb}~Jor2>%!WRZ3b9FJD7Km534VyV(_QO7yG_ts`C1#Gb z$~5d^w9=iKvNUh)dbDjQA9T8l zIJ(5~A?IzaA7)L@^P+y9I&~0KQWN-W_Tscd6eKQRamMSd>*L1a>cO{q#a3QwH3WN! zO9xZE?nseTYsy9VHE)0p_m)~|f+y~n5sYT-#?9r}x*rsV-pn~x9I+N}QLnpbkAlG8 z8sE$VTX?n?zw~Rzo32Yu_Z7$aHG$mTG$mJBR_Yp(rfxNRbi3TIP?|>4Zb*~-z&SOS ztBj+g?DCt%+Bgd%(8+u~Hq8+b$kO-h=+<)`DG7FP>*?Hb%cT#5r32z#f26;H9}DYq znX5vz*TgURJ!n3y+i*r1`OLfHJ*}VXa(xmt-ZG`i#qIb|rQ_kg?iLH_6hZN!E2U+Z z>W9P%H(FQIj*2T`1YfNISql_sW2m#W~>r)2#)P;(LrE#KtJ zs>MI?J2>qSj(uL1rTHw3>_-xGgNBQ!K%>7=)j8*1B%a)r@daW>?4xy>YEBPXT@dD>cN6q1XJyhzjk#6w zL147!DNb|era`s7h$`P3HPla`z&WO6M-MdQYBX=>3yA?tkazRbuWN;0@3!_OeCveo zV9IoR`=Mw#V=}i4&Z_9#Q^b2tV@P<{p4Q_}bp*Lj%ZTcnKYZoU2ZZX$D zwTP25_#&N|#R{-kMD)HK+D5&6qy_wS4`S8vJkQLO?q)X!du|YaTvZ6ZJ@riR-h=yw ziV9g(JEt+C_jwK3wS*8Kq%}7s*8QlECjEfb8X}L4mdn1;yWL9qjRtsGA*L&jT<1w7lm%SH3_|71xEf@q)oCstEia31^lz_`>W)KGD_as zR~d3_*nf&^X-$}lt@qg+Xjm+ajSvT3K+$A;Ms$0a;P1rOhk%+EPJETR?A^)=WD;)Uj~q*X-L|lWpRjG6vF31*vE7yiC@uEfv!b0TPNa zK2>rXeE(LXtpx#!zV;x~Sy_TDf}E`Ub~aR&p4hit$$ytpD+z#Rn_fH@pQBqAz@GJz zP3EeK`S?UgsQr5@p=aCD{~grjCV*Ut#%LZufA9}+fc_pBUw*_`f2VdeoaQ$hy;o9QUCvv%I+FGdz=e&$gp1RWfi`#Ey42lc?#VCFn4LUxEm1Y zX{|sfpX6Y-*EjaeVz@|Vr?j+q+FSdY9@W&;G|)9t|ACGu8Ds79j5YF85DRXQ)j<)IH4iiA}IdMDMKq)cy6EKsv)!*adAIkW6Uk+nDlhgXZ z+Mdl^gr#wXOo9}7jpTMVL=;>zVE#K`{;uI2Omr%8Hp}n^2pzCYyK#@P zT-P{&rPV);vO?4M62RKda?0EgKYf~=TB9*eB<4|huOlkCfZa4bmHB|N24B(hXm{>A zRRFtp@RqhcSI9#d`RTX{hG6&t2C#6qUz5rD-Ly1G*H9~wVtk3|H>#)r;FZ+UeQy`( z>RD-NL%{53smQ_EnM0C5@Kjz#;MJl4E42qn{@-^k=aC~<4Ga!R0^^*Mdk>7md#5J# zD8q63=;Oer_V3>h3+v zhsWPPv~3rg0gPsz;pfl`4cAZKdZW*}6!y23+Dkof#y1BzLQMRQT;IWX!C%rD*k0YQ zNC5BM=yDor?+KxH9D>-+Q1x=Abc5ur%boqd86=BGTp>;%g(M@Hoek{;x}CoPEil5@ zIF@l6fDpK+V&w5n=ba6&dpHT(>(c*@^AGjF`5RQ|WgZI{y#io;B>u0~r(&wgziW8- zPUdcFnQ3VmfO>sE&-li5#>xr+^tqaQ4S9q&ScdfW)WSZX-qF{)m6tKopKh>Rw2(Z# zVe|v=|4(xzBM2bP%dAkcjQ{bgmN0!gdF+1R!x$l3lHC*r zlu__=#`a~$(tsve|dzzWE#y`8UL#n zz~6nJKL`Bx-T+%)V*XY5qix2}1pQLb>AZ)OQ^^SlqRb%#W?T7xS(C>=wx8#-d3O81 zsPEN{*ZKKZJUl!c&)>eRXu^=A{cFjD-tFA#Z1SjBeILV)8ozc5P^<(za*u?G_J1h& zKYkVBU{2lM(&SHlCp{=h{kJtWlLnAy7R0(^`|kYD#dsmP6YxJJ6*_zwpGR9ycz5F= zSIC`x|DrS7BrU*H+nYstB7Xl#vomz@e%Iq*3o2UG`y8oRfRA@!f5Uy$P9?YBuI-~D zj=KjMiV8gp81^RFQn3(dcV+mEG{^Ia#il!mk^ix|xBvPg4sZeguLK1SX_pR9+`r6Q z__amEFM72}uZn2+_QC#RtzJ|CHuv|PJhs-;wql1xguN%pll(7(pQJ0@s5kZWzZir@ z1}0a-^7jL<2)zLMIECdVP1=queg5|K#ESq*?>2dcmSVE8aaq^BtbAiT^D5O+Si( z6eCU^yp19W)OJyKDEQ|ck~16aiFa~90NyE8?|J`!tNVXl8rck>i{VYh7vNP^qT9~@ zJT$gxu^5OAI7rZzlldS1^dx$NMuCx)+Iq*~<-5sy_y2Dn<6pNkfI(SJ?TPwc0RX3ANi6O8 zC+QvO7a{sY<)xU`!{=3hVgyT60q9o&MDG**a7@_xlm8PL;=k^d8EKvhy%NyT3FK?t z1grj}T|I!&Z)du3y~1so8>OQEgGrRpgIWXi9?YWsPQ$`QN%04^Bz%cdONRFb_S*a( zWf%&2*Y$BNFD(O6F@eh;cmA2${$jTQDl3ml8vlXHGV(xs3}Mn!MgSs%cK86 z`42Y!H97oWk#ux-yLu7j0D~7}<469ZT=WxTLg&R72S8NRjGXWMft*abG<R4Fe@X}_sQWa|*ne2` zkfa~rzvM7H!TQ5;pArs`1bUiNO4O1mpzwzLfx&_LIc`c878U>)XESu}IVnGz z-PV6t3WynE12X*o0LOCcbK^o~6$K89}8Y2TD< z_=CZR`T`XmKE5kV@-yqzok~vd5@%waS1gOpas0*UY?Dfdg-q}mZ*J@zf4%*Q*LBuB zRyx^nn_tX?*HoH5+BSIhnr;fhj$Y@4uDuyC^%nr1Wm0Fqk9p&^29172BCkGHGEkaI z-17g1Zmp5esY- zq9pE8P&%pU&wNKk?9V}8-?Qp;bM)#!CAIH8eMvlg>EypwmE!m8u4L7X#w!38Qb8$y z?{S@Y#(jTzTiFwF%a7q##%@n7`nmz*An&UH41v{1Z+xxi7`&*y2;a!wvUdkKut5BsDHN1trc>6I|#ta`dT8^%cP!TeAs(goHn*0{2WPfdI8U|(wD*s5o5T$ zIrybi?+dv7Yg)7$hmNIh!b{R@f0)FLwmlHv1`xjjbrvZDo(2T!)mHmrn)iZk^e<>qF*K0pkr!99 zqHB6DyMo1}a7JJn`c*|b!UHQuU0Fe#yDJ#}B3L+)VuToqwu3qmR%4v+B>A+BGSxZ@ z0i#)L>|xMNa=nAhf8flutkLMj_Y?xg05N5BUOv7K3OfzDCv6{e`h+Z$;&-mJ$Pal) zX#K?6Zda738nW5X=KRE0_V*s!#gYsKyy}4j{Tpp7<$|vz8&{uDhm%Q(P_aL2(6BQ~ zSLK&VS`#EKX6p-Xfd@4@0aj9+}6+ik$dT|Cvyk7lIc zwWXoCW4iU)@`l^s9}d8T^&}K5zC?XOxag;B((Eki$>q0xpWMHPkgW1}-v9aC7`xEF zYyvS2>YIsq9|+jV=gaody#uUY@gknCa4|1zbRL=>p6M`>#NyuD*ux8gS@&;}x7VS& zgdF02tK1+U1fZja79>U!g({AR4TPMd;wf&PkNbMQ!q^)y$Ml5l|ieafQESET-gwl=FXu zX@F3*0Mf-s0tngjf)LLMiNzn?XeMH30mLG-`M_xbyewc09eKRsFSd?>No02ScZ($WGf z{4pH&apd_ds^?+1+1^plQklo7*Qv=CBsxBs&ecXewvTnA{wpvz+2CIj=!leZW5b(loi^l5QPSfkG> zZR9r0CuO?7D|}YiCmAZ5*XR^};*DDb5b}*WoVP;HH(5YVgZ0Efqc@S!##jHbs}6|y z5Mj&is=FeW-6zy(epq%a;@RAU?8+h#`A9t5`*GGB%&1Zz_N^ix250ZaVA7yD^Ywev*#v4$2{z zPM1%GWJZ~49LKxINUhN}EinX9v-gTRWxqp!uy*9ZIS*juA!kM)BQe>Jm=_z#RbF ztEfG$@dR92mhL5sp2eg0`%AHFk7kp33Y%&!b}k~yM&m}o_A#u?fFCQT;GFgwd;JT< zE&@3Y?K@T62G5hvQfxMIJ+ktp>^`|KH~70DADhoZ^+&@oP=j zzEkn1N8V)T%_W?BAE;aVpmyb~izm|*VVM4q;#rzE4=5ZhencoDkTeqw`rZ?7_8;tZmqq!U4@;bSjLB

}JaKHqb=vop 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 B*@gfB From 0e91329fc3782ac0eefc6f35fb72a119dd802a91 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 17 Jun 2021 15:45:41 +0300 Subject: [PATCH 85/96] Fix docs --- docs/developer/plugin-list.asciidoc | 2 +- x-pack/plugins/actions/README.md | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 874aa19828ff8..6675a8dd283c9 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -321,7 +321,7 @@ which will load the visualization's editor. |{kib-repo}blob/{branch}/x-pack/plugins/actions/README.md[actions] -|****# Kibana Actions +|The Kibana actions plugin provides a framework to create executable actions. You can: |{kib-repo}blob/{branch}/x-pack/plugins/alerting/README.md[alerting] diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 131935213d023..f6676718411f4 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -1,4 +1,4 @@ -****# Kibana Actions +# Kibana Actions The Kibana actions plugin provides a framework to create executable actions. You can: @@ -14,6 +14,24 @@ The Kibana actions plugin provides a framework to create executable actions. You Table of Contents +- [Kibana Actions](#kibana-actions) + - [Terminology](#terminology) + - [Usage](#usage) + - [Kibana Actions Configuration](#kibana-actions-configuration) + - [Configuration Options](#configuration-options) + - [**allowedHosts** configuration](#allowedhosts-configuration) + - [Configuration Utilities](#configuration-utilities) + - [Action types](#action-types) + - [Methods](#methods) + - [Executor](#executor) + - [Example](#example) + - [RESTful API](#restful-api) + - [Firing actions](#firing-actions) + - [Accessing a scoped ActionsClient](#accessing-a-scoped-actionsclient) + - [actionsClient.enqueueExecution(options)](#actionsclientenqueueexecutionoptions) + - [Example](#example-1) + - [actionsClient.execute(options)](#actionsclientexecuteoptions) + - [Example](#example-2) - [Built-in Action Types](#built-in-action-types) - [ServiceNow](#servicenow) - [`params`](#params) From d3677d13af69a464e44c6f5a0759fe52eff83024 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 17 Jun 2021 17:33:34 +0300 Subject: [PATCH 86/96] Remove alert source --- .../connectors/action-types/swimlane.asciidoc | 6 --- .../builtin_action_types/swimlane/api.test.ts | 1 - .../swimlane/helpers.test.ts | 4 -- .../builtin_action_types/swimlane/helpers.ts | 1 - .../builtin_action_types/swimlane/mocks.ts | 22 +++----- .../builtin_action_types/swimlane/schema.ts | 2 - .../swimlane/service.test.ts | 3 -- .../components/connectors/swimlane/index.ts | 1 - .../builtin_action_types/swimlane/helpers.ts | 3 +- .../builtin_action_types/swimlane/mocks.ts | 7 --- .../swimlane/steps/swimlane_fields.tsx | 25 --------- .../swimlane/swimlane.test.tsx | 2 - .../swimlane/swimlane.tsx | 5 -- .../swimlane/swimlane_connectors.test.tsx | 2 - .../swimlane/swimlane_params.test.tsx | 2 - .../swimlane/swimlane_params.tsx | 52 ++----------------- .../builtin_action_types/swimlane/types.ts | 1 - .../actions/builtin_action_types/swimlane.ts | 6 --- .../actions/builtin_action_types/swimlane.ts | 7 --- 19 files changed, 12 insertions(+), 140 deletions(-) diff --git a/docs/management/connectors/action-types/swimlane.asciidoc b/docs/management/connectors/action-types/swimlane.asciidoc index ed9a21605b4ea..88447bb496a86 100644 --- a/docs/management/connectors/action-types/swimlane.asciidoc +++ b/docs/management/connectors/action-types/swimlane.asciidoc @@ -36,11 +36,6 @@ API token:: Swimlane API authentication token for HTTP Basic authentication. id: agp4s key: alert-id name: Alert ID - alertSourceConfig: - fieldType: text - id: adnls - key: alert-source - name: Alert Source caseIdConfig: fieldType: text id: ae1mi @@ -104,7 +99,6 @@ image::management/connectors/images/swimlane-params-test.png[Swimlane params tes Swimlane actions have the following configuration properties. -Alert Source:: The alert source of the incident. Comments:: Additional information for the client, such as how to troubleshoot the issue. Severity:: The severity of the incident. diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts index 13c62b9ef1cf8..1e633e2175808 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts @@ -101,7 +101,6 @@ describe('api', () => { expect(externalService.createRecord).toHaveBeenCalledWith({ incident: { alertId: '123456', - alertSource: 'elastic', caseId: '123456', caseName: 'case name', description: 'case desc', diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts index 3ba696f973f1f..423b3dcb3fb64 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts @@ -16,7 +16,6 @@ describe('Create Record Mapping', () => { alertId: 'al123', ruleName: 'Rule Name', severity: 'Critical', - alertSource: 'Elastic', caseName: 'Case Name', caseId: 'es3456789', description: 'case desc', @@ -26,7 +25,6 @@ describe('Create Record Mapping', () => { const data = getBodyForEventAction(appId, mappings, params); expect(data.applicationId).toEqual(appId); expect(data.id).not.toBeDefined(); - expect(data.values?.[mappings.alertSourceConfig?.id ?? 0]).toEqual(params.alertSource); expect(data.values?.[mappings.ruleNameConfig.id]).toEqual(params.ruleName); expect(data.values?.[mappings.caseNameConfig?.id ?? 0]).toEqual(params.caseName); expect(data.values?.[mappings.caseIdConfig?.id ?? 0]).toEqual(params.caseId); @@ -39,7 +37,6 @@ describe('Create Record Mapping', () => { alertId: 'al123', ruleName: 'Rule Name', severity: 'Critical', - alertSource: 'Elastic', caseName: 'Case Name', caseId: 'es3456789', description: 'case desc', @@ -54,7 +51,6 @@ describe('Create Record Mapping', () => { alertId: 'al123', ruleName: 'Rule Name', severity: 'Critical', - alertSource: 'Elastic', caseName: 'Case Name', caseId: 'es3456789', description: 'case desc', diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts index dc18f0a29e6d1..262b3f22faab4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts @@ -14,7 +14,6 @@ type ConfigMapping = Omit, 'commentsConfig'>; const mappingKeysToIncidentKeys: Record = { ruleNameConfig: 'ruleName', alertIdConfig: 'alertId', - alertSourceConfig: 'alertSource', caseIdConfig: 'caseId', caseNameConfig: 'caseName', severityConfig: 'severity', diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts index 860dd38f4fca9..f9931049d81c2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts @@ -8,12 +8,6 @@ import { ExecutorSubActionPushParams, ExternalService, PushToServiceApiParams } from './types'; export const applicationFields = [ - { - id: 'adnjls', - name: 'Alert Source', - key: 'alert-source', - fieldType: 'text', - }, { id: 'adnlas', name: 'Severity', @@ -59,14 +53,13 @@ export const applicationFields = [ ]; export const mappings = { - alertSourceConfig: applicationFields[0], - severityConfig: applicationFields[1], - ruleNameConfig: applicationFields[2], - caseIdConfig: applicationFields[3], - caseNameConfig: applicationFields[4], - commentsConfig: applicationFields[5], - descriptionConfig: applicationFields[6], - alertIdConfig: applicationFields[7], + severityConfig: applicationFields[0], + ruleNameConfig: applicationFields[1], + caseIdConfig: applicationFields[2], + caseNameConfig: applicationFields[3], + commentsConfig: applicationFields[4], + descriptionConfig: applicationFields[5], + alertIdConfig: applicationFields[6], }; export const getApplicationResponse = { fields: applicationFields }; @@ -108,7 +101,6 @@ const executorParams: ExecutorSubActionPushParams = { alertId: '123456', caseName: 'case name', severity: 'critical', - alertSource: 'elastic', caseId: '123456', description: 'case desc', externalId: 'incident-3', diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts index 69f8e4e16bdcf..7f4bdc8ca6c0d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -19,7 +19,6 @@ export const ConfigMapSchema = schema.object(ConfigMap); export const ConfigMapping = { ruleNameConfig: schema.nullable(ConfigMapSchema), alertIdConfig: schema.nullable(ConfigMapSchema), - alertSourceConfig: schema.nullable(ConfigMapSchema), caseIdConfig: schema.nullable(ConfigMapSchema), caseNameConfig: schema.nullable(ConfigMapSchema), commentsConfig: schema.nullable(ConfigMapSchema), @@ -47,7 +46,6 @@ export const SwimlaneSecretsConfigurationSchema = schema.object(SwimlaneSecretsC const SwimlaneFields = { alertId: schema.nullable(schema.string()), ruleName: schema.nullable(schema.string()), - alertSource: schema.nullable(schema.string()), caseId: schema.nullable(schema.string()), caseName: schema.nullable(schema.string()), severity: schema.nullable(schema.string()), diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts index bee13dd081b83..84409fc236a6c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts @@ -47,7 +47,6 @@ describe('Swimlane Service', () => { const incident = { ruleName: 'Rule Name', - alertSource: 'Alert Source', caseId: 'Case Id', caseName: 'Case Name', severity: 'Severity', @@ -184,7 +183,6 @@ describe('Swimlane Service', () => { applicationId: config.appId, values: { [mappings.ruleNameConfig.id]: 'Rule Name', - [mappings.alertSourceConfig.id]: 'Alert Source', [mappings.caseNameConfig.id]: 'Case Name', [mappings.caseIdConfig.id]: 'Case Id', [mappings.severityConfig.id]: 'Severity', @@ -254,7 +252,6 @@ describe('Swimlane Service', () => { id: incidentId, values: { [mappings.ruleNameConfig.id]: 'Rule Name', - [mappings.alertSourceConfig.id]: 'Alert Source', [mappings.caseNameConfig.id]: 'Case Name', [mappings.caseIdConfig.id]: 'Case Id', [mappings.severityConfig.id]: 'Severity', diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts index 0503266ba3523..bd2eaae9e0174 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts @@ -19,7 +19,6 @@ export const getCaseConnector = (): CaseConnector => { }; export const fieldLabels = { - alertSource: i18n.ALERT_SOURCE_LABEL, caseId: i18n.CASE_ID_LABEL, caseName: i18n.CASE_NAME_LABEL, severity: i18n.SEVERITY_LABEL, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts index 91c95c2e5ba7c..0116984ba9fce 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts @@ -15,7 +15,7 @@ const casesRequiredFields = [ 'caseIdConfig', ]; const casesFields = [...casesRequiredFields]; -const alertsRequiredFields = ['ruleNameConfig', 'alertIdConfig', 'alertSourceConfig']; +const alertsRequiredFields = ['ruleNameConfig', 'alertIdConfig']; const alertsFields = ['severityConfig', 'commentsConfig', ...alertsRequiredFields]; const translationMapping: Record = { @@ -25,7 +25,6 @@ const translationMapping: Record = { descriptionConfig: i18n.SW_REQUIRED_DESCRIPTION, commentsConfig: i18n.SW_REQUIRED_COMMENTS, ruleNameConfig: i18n.SW_REQUIRED_RULE_NAME, - alertSourceConfig: i18n.SW_REQUIRED_ALERT_SOURCE, severityConfig: i18n.SW_REQUIRED_SEVERITY, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts index 4ed9d47178a48..15424f9df8305 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts @@ -12,12 +12,6 @@ export const applicationFields = [ key: 'alert-id', fieldType: 'text', }, - { - id: 'adnjls', - name: 'Alert Source', - key: 'alert-source', - fieldType: 'text', - }, { id: 'adnlas', name: 'Severity', @@ -58,7 +52,6 @@ export const applicationFields = [ export const mappings = { alertIdConfig: applicationFields[0], - alertSourceConfig: applicationFields[0], severityConfig: applicationFields[1], ruleNameConfig: applicationFields[2], caseIdConfig: applicationFields[3], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index f1b0945af8f4a..87d0964322e14 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -86,7 +86,6 @@ const SwimlaneFieldsComponent: React.FC = ({ const state = useMemo( () => ({ - alertSourceConfig: createSelectedOption(mappings?.alertSourceConfig), alertIdConfig: createSelectedOption(mappings?.alertIdConfig), severityConfig: createSelectedOption(mappings?.severityConfig), ruleNameConfig: createSelectedOption(mappings?.ruleNameConfig), @@ -201,30 +200,6 @@ const SwimlaneFieldsComponent: React.FC = ({ )} - {isValidFieldForConnector( - connectorType as SwimlaneConnectorType.All, - 'alertSourceConfig' - ) && ( - <> - - editMappings('alertSourceConfig', e)} - isInvalid={mappingErrors?.alertSourceConfig != null && !hasChangedConnectorType} - /> - - - )} {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'severityConfig') && ( <> { connectorType: 'all', mappings: { alertIdConfig: { id: '1234' }, - alertSourceConfig: { id: '1234' }, severityConfig: { id: '1234' }, ruleNameConfig: { id: '1234' }, caseIdConfig: { id: '1234' }, @@ -143,7 +142,6 @@ describe('swimlane connector validation', () => { { alertIdConfig: 'Alert ID is required.', ruleNameConfig: 'Rule name is required.', - alertSourceConfig: 'Alert source is required.', }, ], connectorType: [], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx index c96fd5737ca4a..5e06e3935eebd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx @@ -83,7 +83,6 @@ export function getActionType(): ActionTypeModel< const errors = { 'subActionParams.incident.ruleName': new Array(), 'subActionParams.incident.alertId': new Array(), - 'subActionParams.incident.alertSource': new Array(), }; const validationResult = { errors, @@ -99,10 +98,6 @@ export function getActionType(): ActionTypeModel< errors['subActionParams.incident.alertId'].push(i18n.SW_REQUIRED_ALERT_ID); } - if (hasIncident && !actionParams.subActionParams.incident.alertSource?.length) { - errors['subActionParams.incident.alertSource'].push(i18n.SW_REQUIRED_ALERT_SOURCE); - } - return validationResult; }, actionConnectorFields: lazy(() => import('./swimlane_connectors')), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx index ded77a8817fcc..8d5c2f0b3536d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx @@ -25,7 +25,6 @@ describe('SwimlaneActionConnectorFields renders', () => { apiUrl: 'http:\\test', appId: '1234567asbd32', mappings: { - alertSourceConfig: { id: '123', key: 'product-source' }, severityConfig: { id: '123', key: 'severity' }, caseNameConfig: { id: '123', key: 'case-name' }, caseIdConfig: { id: '123', key: 'case-id' }, @@ -88,7 +87,6 @@ describe('SwimlaneActionConnectorFields renders', () => { apiUrl: 'http:\\test', appId: '1234567asbd32', mappings: { - alertSourceConfig: { id: '123', key: 'product-source' }, severityConfig: { id: '123', key: 'severity' }, caseNameConfig: { id: '123', key: 'case-name' }, caseIdConfig: { id: '123', key: 'case-id' }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx index 8cbc0a8656416..6e993ee89692d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx @@ -18,7 +18,6 @@ describe('SwimlaneParamsFields renders', () => { subActionParams: { incident: { ruleName: 'rule name', - alertSource: 'alert source', caseId: '3456789', caseName: 'my case name', severity: 'critical', @@ -54,6 +53,5 @@ describe('SwimlaneParamsFields renders', () => { expect(wrapper.find('[data-test-subj="severity"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="comments"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="alertSource"]').exists()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx index 796348e98f264..9bd14a06d657a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx @@ -5,36 +5,19 @@ * 2.0. */ -import React, { memo, useCallback, useEffect, useRef, useMemo } from 'react'; -import { - EuiCallOut, - EuiFormRow, - EuiIconTip, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; +import React, { useCallback, useEffect, useRef, useMemo } from 'react'; +import { EuiCallOut, EuiFormRow, EuiSpacer } from '@elastic/eui'; import * as i18n from './translations'; import { ActionParamsProps } from '../../../../types'; import { SwimlaneActionConnector, SwimlaneActionParams, SwimlaneConnectorType } from './types'; import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; -const AlertSourceLabel = memo(() => ( - - {i18n.SW_ALERT_SOURCE_FIELD_LABEL} - - - - -)); - const SwimlaneParamsFields: React.FunctionComponent> = ({ actionParams, editAction, index, messageVariables, - errors, actionConnector, }) => { const { incident, comments } = useMemo( @@ -53,18 +36,16 @@ const SwimlaneParamsFields: React.FunctionComponent ({ hasAlertId: mappings.alertIdConfig != null, hasRuleName: mappings.ruleNameConfig != null, - hasAlertSource: mappings.alertSourceConfig != null, hasComments: mappings.commentsConfig != null, hasSeverity: mappings.severityConfig != null, }), [ mappings.alertIdConfig, mappings.ruleNameConfig, - mappings.alertSourceConfig, mappings.commentsConfig, mappings.severityConfig, ] @@ -76,7 +57,7 @@ const SwimlaneParamsFields: React.FunctionComponent { @@ -140,31 +121,6 @@ const SwimlaneParamsFields: React.FunctionComponent - {hasAlertSource && ( - <> - } - error={errors['subActionParams.incident.alertSource'] as string[]} - isInvalid={ - errors['subActionParams.incident.alertSource'] !== undefined && - errors['subActionParams.incident.alertSource'].length > 0 && - incident.alertSource !== undefined - } - > - - - - - )} {hasSeverity && ( <> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts index 50323fec30a49..501ebc759d76f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts @@ -22,7 +22,6 @@ export interface SwimlaneConfig { } export interface SwimlaneMappingConfig { - alertSourceConfig: SwimlaneFieldMappingConfig; alertIdConfig: SwimlaneFieldMappingConfig; severityConfig: SwimlaneFieldMappingConfig; caseNameConfig: SwimlaneFieldMappingConfig; diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts index 6e25c8bd2d288..95e041bbeb03a 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts @@ -23,12 +23,6 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { appId: '123456asdf', connectorType: 'all', mappings: { - alertSourceConfig: { - id: 'adnjls', - name: 'Alert Source', - key: 'alert-source', - fieldType: 'text', - }, severityConfig: { id: 'adnlas', name: 'Severity', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts index 39f542e7ba53d..dacfdccda02ed 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts @@ -33,12 +33,6 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { key: 'alert-id', fieldType: 'text', }, - alertSourceConfig: { - id: 'adnjls', - name: 'Alert Source', - key: 'alert-source', - fieldType: 'text', - }, severityConfig: { id: 'adnlas', name: 'Severity', @@ -87,7 +81,6 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { alertId: 'fs345f78g', ruleName: 'Rule Name', severity: 'Critical', - alertSource: 'Elastic', caseName: 'Case Name', caseId: 'es3456789', description: 'This is a description', From 17ffdd5eb9d2e79c98b9678dcea9b8f758c5cc8f Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 18 Jun 2021 12:14:13 +0300 Subject: [PATCH 87/96] Add more tests --- .../jira/jira_connectors.test.tsx | 2 +- .../resilient/resilient_connectors.test.tsx | 2 +- .../servicenow/servicenow_connectors.test.tsx | 2 +- .../builtin_action_types/swimlane/api.test.ts | 60 +++++ .../builtin_action_types/swimlane/mocks.ts | 4 +- .../swimlane/steps/swimlane_connection.tsx | 6 +- .../swimlane/swimlane_connectors.test.tsx | 244 ++++++++++++++++-- .../swimlane/swimlane_params.test.tsx | 154 ++++++++--- 8 files changed, 414 insertions(+), 60 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx index b89f71b0fc354..be5250ccf8b29 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx @@ -12,7 +12,7 @@ import { JiraActionConnector } from './types'; jest.mock('../../../../common/lib/kibana'); describe('JiraActionConnectorFields renders', () => { - test('alerting Jira connector fields is rendered', () => { + test('alerting Jira connector fields are rendered', () => { const actionConnector = { secrets: { email: 'email', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx index b7b68b9485d8a..bbd237a7cec89 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx @@ -12,7 +12,7 @@ import { ResilientActionConnector } from './types'; jest.mock('../../../../common/lib/kibana'); describe('ResilientActionConnectorFields renders', () => { - test('alerting Resilient connector fields is rendered', () => { + test('alerting Resilient connector fields are rendered', () => { const actionConnector = { secrets: { apiKeyId: 'key', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx index 330844b93b6b5..4993c51f350ad 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx @@ -12,7 +12,7 @@ import { ServiceNowActionConnector } from './types'; jest.mock('../../../../common/lib/kibana'); describe('ServiceNowActionConnectorFields renders', () => { - test('alerting servicenow connector fields is rendered', () => { + test('alerting servicenow connector fields are rendered', () => { const actionConnector = { secrets: { username: 'user', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts index 5919ea0bc8215..90bab65b83bfd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts @@ -81,5 +81,65 @@ describe('Swimlane API', () => { expect(e.message).toContain('bad'); } }); + + it('it removes unsafe fields', async () => { + const abortCtrl = new AbortController(); + + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + fields: [ + { + id: '__proto__', + name: 'Alert Id', + key: 'alert-id', + fieldType: 'text', + }, + { + id: 'a6ide', + name: '__proto__', + key: 'alert-id', + fieldType: 'text', + }, + { + id: 'a6ide', + name: 'Alert Id', + key: '__proto__', + fieldType: 'text', + }, + { + id: 'a6ide', + name: 'Alert Id', + key: 'alert-id', + fieldType: '__proto__', + }, + { + id: 'safe-id', + name: 'Safe', + key: 'safe-key', + fieldType: 'safe-text', + }, + ], + }), + }); + + const res = await getApplication({ + signal: abortCtrl.signal, + apiToken: '', + appId: '', + url: '', + }); + + expect(res).toEqual({ + fields: [ + { + id: 'safe-id', + name: 'Safe', + key: 'safe-key', + fieldType: 'safe-text', + }, + ], + }); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts index 15424f9df8305..1574dfe2f5384 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts @@ -39,8 +39,8 @@ export const applicationFields = [ { id: 'a6fdf', name: 'Comments', - key: 'comments', - fieldType: 'notes', + key: 'notes', + fieldType: 'comments', }, { id: 'a6fde', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx index d03be0b68ecd8..a3cbdc840e4fe 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx @@ -187,7 +187,11 @@ const SwimlaneConnectionComponent: React.FunctionComponent = ({ - + {i18n.SW_RETRIEVE_CONFIGURATION_LABEL} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx index 8d5c2f0b3536d..6740179d786f2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx @@ -10,10 +10,24 @@ import { mountWithIntl, nextTick } from '@kbn/test/jest'; import { act } from 'react-dom/test-utils'; import { SwimlaneActionConnector } from './types'; import SwimlaneActionConnectorFields from './swimlane_connectors'; +import { useGetApplication } from './use_get_application'; +import { applicationFields, mappings } from './mocks'; + jest.mock('../../../../common/lib/kibana'); +jest.mock('./use_get_application'); + +const useGetApplicationMock = useGetApplication as jest.Mock; +const getApplication = jest.fn(); describe('SwimlaneActionConnectorFields renders', () => { - test('all connector fields is rendered', async () => { + beforeAll(() => { + useGetApplicationMock.mockReturnValue({ + getApplication, + isLoading: false, + }); + }); + + test('all connector fields are rendered', async () => { const actionConnector = { secrets: { apiToken: 'test', @@ -24,11 +38,8 @@ describe('SwimlaneActionConnectorFields renders', () => { config: { apiUrl: 'http:\\test', appId: '1234567asbd32', - mappings: { - severityConfig: { id: '123', key: 'severity' }, - caseNameConfig: { id: '123', key: 'case-name' }, - caseIdConfig: { id: '123', key: 'case-id' }, - }, + connectorType: 'all', + mappings, }, } as SwimlaneActionConnector; @@ -47,12 +58,9 @@ describe('SwimlaneActionConnectorFields renders', () => { wrapper.update(); }); - expect(wrapper.find('[data-test-subj="swimlaneApiUrlInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="swimlaneApiUrlInput"]').first().prop('value')).toBe( - 'http:\\test' - ); - expect(wrapper.find('[data-test-subj="swimlaneAppIdInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="swimlaneApiTokenInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneApiUrlInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneAppIdInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneApiTokenInput"]').exists()).toBeTruthy(); }); test('should display a message on create to remember credentials', () => { @@ -86,11 +94,8 @@ describe('SwimlaneActionConnectorFields renders', () => { config: { apiUrl: 'http:\\test', appId: '1234567asbd32', - mappings: { - severityConfig: { id: '123', key: 'severity' }, - caseNameConfig: { id: '123', key: 'case-name' }, - caseIdConfig: { id: '123', key: 'case-id' }, - }, + connectorType: 'all', + mappings, }, } as SwimlaneActionConnector; @@ -106,4 +111,209 @@ describe('SwimlaneActionConnectorFields renders', () => { expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); }); + + test('renders the mappings correctly - connector type all', async () => { + getApplication.mockResolvedValue({ + fields: applicationFields, + }); + + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'all', + mappings, + }, + } as SwimlaneActionConnector; + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + + await act(async () => { + wrapper.find('[data-test-subj="swimlaneConfigureMapping"]').first().simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneSeverityInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').exists()).toBeTruthy(); + }); + + test('renders the mappings correctly - connector type cases', async () => { + getApplication.mockResolvedValue({ + fields: applicationFields, + }); + + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'cases', + mappings, + }, + } as SwimlaneActionConnector; + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + + await act(async () => { + wrapper.find('[data-test-subj="swimlaneConfigureMapping"]').first().simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="swimlaneSeverityInput"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').exists()).toBeTruthy(); + }); + + test('renders the mappings correctly - connector type alerts', async () => { + getApplication.mockResolvedValue({ + fields: applicationFields, + }); + + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'alerts', + mappings, + }, + } as SwimlaneActionConnector; + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + + await act(async () => { + wrapper.find('[data-test-subj="swimlaneConfigureMapping"]').first().simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneSeverityInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').exists()).toBeFalsy(); + }); + + test('renders the correct options per field', async () => { + getApplication.mockResolvedValue({ + fields: applicationFields, + }); + + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'all', + mappings, + }, + } as SwimlaneActionConnector; + + const textOptions = [ + { label: 'Alert Id (alert-id)', value: 'a6ide' }, + { label: 'Severity (severity)', value: 'adnlas' }, + { label: 'Rule Name (rule-name)', value: 'adnfls' }, + { label: 'Case Id (case-id-name)', value: 'a6sst' }, + { label: 'Case Name (case-name)', value: 'a6fst' }, + { label: 'Description (description)', value: 'a6fde' }, + ]; + + const commentOptions = [{ label: 'Comments (notes)', value: 'a6fdf' }]; + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + + await act(async () => { + wrapper.find('[data-test-subj="swimlaneConfigureMapping"]').first().simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').first().prop('options')).toEqual( + textOptions + ); + expect( + wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').first().prop('options') + ).toEqual(textOptions); + expect( + wrapper.find('[data-test-subj="swimlaneSeverityInput"]').first().prop('options') + ).toEqual(textOptions); + expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').first().prop('options')).toEqual( + textOptions + ); + expect( + wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').first().prop('options') + ).toEqual(textOptions); + expect( + wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').first().prop('options') + ).toEqual(commentOptions); + expect( + wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').first().prop('options') + ).toEqual(textOptions); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx index 6e993ee89692d..32cf2c3c786d3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx @@ -12,46 +12,126 @@ import { SwimlaneConnectorType } from './types'; import { mappings } from './mocks'; describe('SwimlaneParamsFields renders', () => { - test('all params fields is rendered', () => { - const actionParams = { - subAction: 'pushToService', - subActionParams: { - incident: { - ruleName: 'rule name', - caseId: '3456789', - caseName: 'my case name', - severity: 'critical', - description: 'case desc', - externalId: null, - alertId: '3456789', - }, - comments: [], + const editAction = jest.fn(); + const actionParams = { + subAction: 'pushToService', + subActionParams: { + incident: { + alertId: '3456789', + ruleName: 'rule name', + severity: 'critical', + caseId: null, + caseName: null, + description: null, + externalId: null, }, - }; - - const connector = { - secrets: {}, - config: { mappings, connectorType: SwimlaneConnectorType.All }, - id: 'test', - actionTypeId: '.test', - name: 'Test', - isPreconfigured: false, - }; - - const wrapper = mountWithIntl( - {}} - index={0} - /> - ); + comments: [], + }, + }; + + const connector = { + secrets: {}, + config: { mappings, connectorType: SwimlaneConnectorType.All }, + id: 'test', + actionTypeId: '.test', + name: 'Test', + isPreconfigured: false, + }; + + const defaultProps = { + actionParams, + errors: { + 'subActionParams.incident.ruleName': [], + 'subActionParams.incident.alertId': [], + }, + editAction, + index: 0, + messageVariables: [], + actionConnector: connector, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('all params fields are rendered', () => { + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="severity"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="comments"]').exists()).toBeTruthy(); }); + + test('it set the correct default params', () => { + mountWithIntl(); + expect(editAction).toHaveBeenCalledWith('subAction', 'pushToService', 0); + expect(editAction).toHaveBeenCalledWith( + 'subActionParams', + { + incident: { alertId: '{{alert.id}}', ruleName: '{{rule.name}}' }, + comments: [], + }, + 0 + ); + }); + + test('it reset the fields when connector changes', () => { + const wrapper = mountWithIntl(); + expect(editAction).not.toHaveBeenCalled(); + + wrapper.setProps({ actionConnector: { ...connector, id: '1234' } }); + expect(editAction).toHaveBeenCalledWith( + 'subActionParams', + { + incident: { alertId: '{{alert.id}}', ruleName: '{{rule.name}}' }, + comments: [], + }, + 0 + ); + }); + + test('it set the severity', () => { + const wrapper = mountWithIntl(); + expect(editAction).not.toHaveBeenCalled(); + + wrapper.setProps({ actionConnector: { ...connector, id: '1234' } }); + expect(editAction).toHaveBeenCalledWith( + 'subActionParams', + { + incident: { alertId: '{{alert.id}}', ruleName: '{{rule.name}}' }, + comments: [], + }, + 0 + ); + }); + + describe('UI updates', () => { + const changeEvent = { target: { value: 'Bug' } } as React.ChangeEvent; + const simpleFields = [ + { dataTestSubj: 'input[data-test-subj="severityInput"]', key: 'severity' }, + ]; + + simpleFields.forEach((field) => + test(`${field.key} update triggers editAction`, () => { + const wrapper = mountWithIntl(); + const theField = wrapper.find(field.dataTestSubj).first(); + theField.prop('onChange')!(changeEvent); + expect(editAction.mock.calls[0][1].incident[field.key]).toEqual(changeEvent.target.value); + }) + ); + + test('A comment triggers editAction', () => { + const wrapper = mountWithIntl(); + const comments = wrapper.find('textarea[data-test-subj="commentsTextArea"]'); + expect(comments.simulate('change', changeEvent)); + expect(editAction.mock.calls[0][1].comments.length).toEqual(1); + }); + + test('An empty comment does not trigger editAction', () => { + const wrapper = mountWithIntl(); + const emptyComment = { target: { value: '' } }; + const comments = wrapper.find('[data-test-subj="commentsTextArea"] textarea'); + expect(comments.simulate('change', emptyComment)); + expect(editAction.mock.calls.length).toEqual(0); + }); + }); }); From ab0fa3851df7b551b546c3c55a9a811084b2ca46 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 18 Jun 2021 16:16:30 +0300 Subject: [PATCH 88/96] Fix tests --- .../components/case_view/index.test.tsx | 11 ++-- .../components/create/connector.test.tsx | 52 ++++++++++--------- .../public/components/create/connector.tsx | 1 + .../public/components/create/form.test.tsx | 6 +++ .../components/edit_connector/index.tsx | 2 +- .../plugins/cases/public/components/utils.ts | 12 ++++- 6 files changed, 54 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/cases/public/components/case_view/index.test.tsx b/x-pack/plugins/cases/public/components/case_view/index.test.tsx index f89218d5407eb..4649339151179 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.test.tsx @@ -633,6 +633,7 @@ describe('CaseView ', () => { ).toBe(connectorName); }); }); + it('should update connector', async () => { const wrapper = mount( @@ -655,15 +656,19 @@ describe('CaseView ', () => { wrapper.find('[data-test-subj="connector-edit"] button').simulate('click'); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click'); - await waitFor(() => wrapper.update()); + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeTruthy(); + }); + wrapper.find(`button[data-test-subj="edit-connectors-submit"]`).first().simulate('click'); await waitFor(() => { - const updateObject = updateCaseProperty.mock.calls[0][0]; + wrapper.update(); expect(updateCaseProperty).toHaveBeenCalledTimes(1); + const updateObject = updateCaseProperty.mock.calls[0][0]; expect(updateObject.updateKey).toEqual('connector'); expect(updateObject.updateValue).toEqual({ id: 'resilient-2', diff --git a/x-pack/plugins/cases/public/components/create/connector.test.tsx b/x-pack/plugins/cases/public/components/create/connector.test.tsx index b2a8838774b09..981d40b8c0f53 100644 --- a/x-pack/plugins/cases/public/components/create/connector.test.tsx +++ b/x-pack/plugins/cases/public/components/create/connector.test.tsx @@ -18,6 +18,9 @@ import { useGetSeverity } from '../connectors/resilient/use_get_severity'; import { useGetChoices } from '../connectors/servicenow/use_get_choices'; import { incidentTypes, severity, choices } from '../connectors/mock'; import { schema, FormProps } from './schema'; +import { TestProviders } from '../../common/mock'; +import { useCaseConfigure } from '../../containers/configure/use_configure'; +import { useCaseConfigureResponse } from '../configure_cases/__mock__'; jest.mock('../../common/lib/kibana', () => { return { @@ -41,10 +44,12 @@ jest.mock('../../common/lib/kibana', () => { jest.mock('../connectors/resilient/use_get_incident_types'); jest.mock('../connectors/resilient/use_get_severity'); jest.mock('../connectors/servicenow/use_get_choices'); +jest.mock('../../containers/configure/use_configure'); const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; const useGetSeverityMock = useGetSeverity as jest.Mock; const useGetChoicesMock = useGetChoices as jest.Mock; +const useCaseConfigureMock = useCaseConfigure as jest.Mock; const useGetIncidentTypesResponse = { isLoading: false, @@ -89,35 +94,30 @@ describe('Connector', () => { useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse); useGetSeverityMock.mockReturnValue(useGetSeverityResponse); useGetChoicesMock.mockReturnValue(useGetChoicesResponse); + useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); }); it('it renders', async () => { const wrapper = mount( - - - + + + + + ); expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="connector-fields"]`).exists()).toBeTruthy(); - - await waitFor(() => { - expect(wrapper.find(`button[data-test-subj="dropdown-connectors"]`).first().text()).toBe( - 'My Connector' - ); - }); - - await waitFor(() => { - wrapper.update(); - expect(wrapper.find(`[data-test-subj="connector-fields-sn-itsm"]`).exists()).toBeTruthy(); - }); + // Selected connector is set to none so no fields should be displayed + expect(wrapper.find(`[data-test-subj="connector-fields"]`).exists()).toBeFalsy(); }); it('it is disabled and loading when isLoadingConnectors=true', async () => { const wrapper = mount( - - - + + + + + ); expect( @@ -131,9 +131,11 @@ describe('Connector', () => { it('it is disabled and loading when isLoading=true', async () => { const wrapper = mount( - - - + + + + + ); expect( @@ -146,9 +148,11 @@ describe('Connector', () => { it(`it should change connector`, async () => { const wrapper = mount( - - - + + + + + ); expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeFalsy(); diff --git a/x-pack/plugins/cases/public/components/create/connector.tsx b/x-pack/plugins/cases/public/components/create/connector.tsx index 3aad3d7610fcc..ada03acba5a81 100644 --- a/x-pack/plugins/cases/public/components/create/connector.tsx +++ b/x-pack/plugins/cases/public/components/create/connector.tsx @@ -48,6 +48,7 @@ const ConnectorFields = ({ const [{ connectorId }] = useFormData({ watch: ['connectorId'] }); const { setValue } = field; let connector = getConnectorById(connectorId, connectors) ?? null; + if ( connector && hideConnectorServiceNowSir && diff --git a/x-pack/plugins/cases/public/components/create/form.test.tsx b/x-pack/plugins/cases/public/components/create/form.test.tsx index 5f3b778a7cafc..783ead9b271fd 100644 --- a/x-pack/plugins/cases/public/components/create/form.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form.test.tsx @@ -17,11 +17,16 @@ import { schema, FormProps } from './schema'; import { CreateCaseForm } from './form'; import { OwnerProvider } from '../owner_context'; import { SECURITY_SOLUTION_OWNER } from '../../../common'; +import { useCaseConfigure } from '../../containers/configure/use_configure'; +import { useCaseConfigureResponse } from '../configure_cases/__mock__'; jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/configure/use_connectors'); +jest.mock('../../containers/configure/use_configure'); + const useGetTagsMock = useGetTags as jest.Mock; const useConnectorsMock = useConnectors as jest.Mock; +const useCaseConfigureMock = useCaseConfigure as jest.Mock; const initialCaseValue: FormProps = { description: '', @@ -54,6 +59,7 @@ describe('CreateCaseForm', () => { jest.resetAllMocks(); useGetTagsMock.mockReturnValue({ tags: ['test'] }); useConnectorsMock.mockReturnValue({ loading: false, connectors: connectorsMock }); + useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); }); it('it renders with steps', async () => { diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.tsx index 8f5f75a7db1d9..c72369dc86894 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.tsx @@ -117,7 +117,7 @@ export const EditConnector = React.memo( schema, }); - const { setFieldValue, submit } = form; + const { setFieldValue, submit, getErrors } = form; const [{ currentConnector, fields, editConnector }, dispatch] = useReducer( editConnectorReducer, diff --git a/x-pack/plugins/cases/public/components/utils.ts b/x-pack/plugins/cases/public/components/utils.ts index dc606f62c03ca..033529c27a2d4 100644 --- a/x-pack/plugins/cases/public/components/utils.ts +++ b/x-pack/plugins/cases/public/components/utils.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { FieldConfig } from '../common/shared_imports'; +import { ConnectorTypes } from '../../common'; +import { FieldConfig, ValidationConfig } from '../common/shared_imports'; import { connectorValidator as swimlaneConnectorValidator } from './connectors/swimlane/validator'; import { CaseActionConnector } from './types'; @@ -14,6 +15,13 @@ export const getConnectorById = ( connectors: CaseActionConnector[] ): CaseActionConnector | null => connectors.find((c) => c.id === id) ?? null; +const validators: Record< + string, + (connector: CaseActionConnector) => ReturnType +> = { + [ConnectorTypes.swimlane]: swimlaneConnectorValidator, +}; + export const getConnectorsFormValidators = ({ connectors = [], config = {}, @@ -27,7 +35,7 @@ export const getConnectorsFormValidators = ({ validator: ({ value: connectorId }) => { const connector = getConnectorById(connectorId as string, connectors); if (connector != null) { - return swimlaneConnectorValidator(connector); + return validators[connector.actionTypeId]?.(connector); } }, }, From ae4a85d0f2d044e860250deb0f5c2a6f8748292a Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 18 Jun 2021 16:33:26 +0300 Subject: [PATCH 89/96] Improve error message --- .../server/builtin_action_types/swimlane/service.test.ts | 6 +++--- .../actions/server/builtin_action_types/swimlane/service.ts | 4 ++-- .../cases/public/components/edit_connector/index.tsx | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts index 84409fc236a6c..861d597d0b81f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts @@ -376,7 +376,7 @@ describe('Swimlane Service', () => { incident, }) ).rejects.toThrow( - `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Error: An error has occurred. Reason: ` + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Error: An error has occurred. Reason: unknown` ); }); @@ -393,7 +393,7 @@ describe('Swimlane Service', () => { incident, }) ).rejects.toThrow( - `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Error: An error has occurred. Reason: ` + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Error: An error has occurred. Reason: unknown` ); }); @@ -410,7 +410,7 @@ describe('Swimlane Service', () => { incident, }) ).rejects.toThrow( - `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Error: An error has occurred. Reason: ` + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Error: An error has occurred. Reason: unknown` ); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index 734cd76033172..5c6ec803303cb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -28,11 +28,11 @@ import * as i18n from './translations'; const createErrorMessage = (errorResponse: ResponseError | null | undefined): string => { if (errorResponse == null) { - return ''; + return 'unknown'; } const { ErrorCode, Argument } = errorResponse; - return Argument != null && ErrorCode != null ? `${Argument} (${ErrorCode})` : ''; + return Argument != null && ErrorCode != null ? `${Argument} (${ErrorCode})` : 'unknown'; }; export const createExternalService = ( diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.tsx index c72369dc86894..8f5f75a7db1d9 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.tsx @@ -117,7 +117,7 @@ export const EditConnector = React.memo( schema, }); - const { setFieldValue, submit, getErrors } = form; + const { setFieldValue, submit } = form; const [{ currentConnector, fields, editConnector }, dispatch] = useReducer( editConnectorReducer, From 223b3347a489ad4db711e13d6a4cef74c17aa90d Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 22 Jun 2021 18:01:38 +0300 Subject: [PATCH 90/96] Close server after the end of tests --- .../tests/actions/builtin_action_types/swimlane.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts index dacfdccda02ed..92e99a9d504f3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts @@ -119,6 +119,13 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { ); }); + after(() => { + swimlaneServer.close(); + if (proxyServer) { + proxyServer.close(); + } + }); + describe('Swimlane - Action Creation', () => { it('should return 200 when creating a swimlane action successfully', async () => { const { body: createdAction } = await supertest @@ -470,11 +477,6 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { }); }); }); - after(() => { - if (proxyServer) { - proxyServer.close(); - } - }); }); }); } From 2672be7d55a7bf608b056e90ea36f22cbc959e60 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 22 Jun 2021 18:21:19 +0300 Subject: [PATCH 91/96] Add status code to error messages --- .../swimlane/service.test.ts | 31 ++++++++++++++----- .../builtin_action_types/swimlane/service.ts | 18 +++++------ 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts index 861d597d0b81f..fd925132f9266 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts @@ -202,7 +202,7 @@ describe('Swimlane Service', () => { }); await expect(service.createRecord({ incident })).rejects.toThrow( - `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Error: An error has occurred.` + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` ); }); }); @@ -271,7 +271,7 @@ describe('Swimlane Service', () => { }); await expect(service.updateRecord({ incident, incidentId })).rejects.toThrow( - `[Action][Swimlane]: Unable to update record in application with id ${config.appId}. Error: An error has occurred.` + `[Action][Swimlane]: Unable to update record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` ); }); }); @@ -338,7 +338,7 @@ describe('Swimlane Service', () => { }); await expect(service.createComment({ comment, incidentId, createdDate })).rejects.toThrow( - `[Action][Swimlane]: Unable to create comment in application with id ${config.appId}. Error: An error has occurred.` + `[Action][Swimlane]: Unable to create comment in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` ); }); }); @@ -359,7 +359,7 @@ describe('Swimlane Service', () => { incident, }) ).rejects.toThrow( - `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Error: An error has occurred. Reason: Invalid field (1)` + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: Invalid field (1)` ); }); @@ -376,7 +376,7 @@ describe('Swimlane Service', () => { incident, }) ).rejects.toThrow( - `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Error: An error has occurred. Reason: unknown` + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` ); }); @@ -393,7 +393,7 @@ describe('Swimlane Service', () => { incident, }) ).rejects.toThrow( - `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Error: An error has occurred. Reason: unknown` + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` ); }); @@ -410,7 +410,24 @@ describe('Swimlane Service', () => { incident, }) ).rejects.toThrow( - `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Error: An error has occurred. Reason: unknown` + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` + ); + }); + + test('it shows the status code', async () => { + requestMock.mockImplementation(() => { + const error = new Error('An error has occurred'); + // @ts-ignore + error.response = { data: errorResponse, status: 400 }; + throw error; + }); + + await expect( + service.createRecord({ + incident, + }) + ).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 400. Error: An error has occurred. Reason: Invalid field (1)` ); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts index 5c6ec803303cb..f68d22121dbcc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -99,9 +99,9 @@ export const createExternalService = ( throw new Error( getErrorMessage( i18n.NAME, - `Unable to create record in application with id ${appId}. Error: ${ - error.message - }. Reason: ${createErrorMessage(error.response?.data)}` + `Unable to create record in application with id ${appId}. Status: ${ + error.response?.status ?? 500 + }. Error: ${error.message}. Reason: ${createErrorMessage(error.response?.data)}` ) ); } @@ -134,9 +134,9 @@ export const createExternalService = ( throw new Error( getErrorMessage( i18n.NAME, - `Unable to update record in application with id ${appId}. Error: ${ - error.message - }. Reason: ${createErrorMessage(error.response?.data)}` + `Unable to update record in application with id ${appId}. Status: ${ + error.response?.status ?? 500 + }. Error: ${error.message}. Reason: ${createErrorMessage(error.response?.data)}` ) ); } @@ -180,9 +180,9 @@ export const createExternalService = ( throw new Error( getErrorMessage( i18n.NAME, - `Unable to create comment in application with id ${appId}. Error: ${ - error.message - }. Reason: ${createErrorMessage(error.response?.data)}` + `Unable to create comment in application with id ${appId}. Status: ${ + error.response?.status ?? 500 + }. Error: ${error.message}. Reason: ${createErrorMessage(error.response?.data)}` ) ); } From 6f6ba26e453aa6204e13ff9d3362a193108bcd11 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 22 Jun 2021 19:03:57 +0300 Subject: [PATCH 92/96] PR feedback --- .../builtin_action_types/swimlane/helpers.ts | 6 ++---- .../cases/server/client/cases/utils.ts | 2 +- .../builtin_action_types/swimlane/helpers.ts | 8 ++++---- .../builtin_action_types/swimlane/types.ts | 20 ++++++++----------- 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts index 262b3f22faab4..13b2df1c97f16 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts @@ -5,11 +5,9 @@ * 2.0. */ -import { TypeOf } from '@kbn/config-schema'; -import { ConfigMappingSchema } from './schema'; -import { CreateRecordParams, Incident, MappingConfigType, SwimlaneRecordPayload } from './types'; +import { CreateRecordParams, Incident, SwimlaneRecordPayload, MappingConfigType } from './types'; -type ConfigMapping = Omit, 'commentsConfig'>; +type ConfigMapping = Omit; const mappingKeysToIncidentKeys: Record = { ruleNameConfig: 'ruleName', diff --git a/x-pack/plugins/cases/server/client/cases/utils.ts b/x-pack/plugins/cases/server/client/cases/utils.ts index ce3917c80651e..f5a10d705e095 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.ts @@ -252,7 +252,7 @@ export const prepareFieldsForTransformation = ({ mappings.reduce( (acc: PipedField[], mapping) => mapping != null && - mapping.target != null && // TODO put warning if no target + mapping.target != null && mapping.target !== 'not_mapped' && mapping.action_type !== 'nothing' && mapping.source !== 'comments' diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts index 0116984ba9fce..413b952675b8c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts @@ -5,17 +5,17 @@ * 2.0. */ -import { SwimlaneConnectorType, SwimlaneMappingConfig } from './types'; +import { SwimlaneConnectorType, SwimlaneMappingConfig, MappingConfigurationKeys } from './types'; import * as i18n from './translations'; -const casesRequiredFields = [ +const casesRequiredFields: MappingConfigurationKeys[] = [ 'caseNameConfig', 'descriptionConfig', 'commentsConfig', 'caseIdConfig', ]; const casesFields = [...casesRequiredFields]; -const alertsRequiredFields = ['ruleNameConfig', 'alertIdConfig']; +const alertsRequiredFields: MappingConfigurationKeys[] = ['ruleNameConfig', 'alertIdConfig']; const alertsFields = ['severityConfig', 'commentsConfig', ...alertsRequiredFields]; const translationMapping: Record = { @@ -30,7 +30,7 @@ const translationMapping: Record = { export const isValidFieldForConnector = ( connector: SwimlaneConnectorType, - field: string + field: MappingConfigurationKeys ): boolean => { if (connector === SwimlaneConnectorType.All) { return true; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts index 501ebc759d76f..f0a54e8b6c3bf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts @@ -5,9 +5,13 @@ * 2.0. */ +/* eslint-disable @kbn/eslint/no-restricted-paths */ + import { UserConfiguredActionConnector } from '../../../../types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ExecutorSubActionPushParams } from '../../../../../../actions/server/builtin_action_types/swimlane/types'; +import { + ExecutorSubActionPushParams, + MappingConfigType, +} from '../../../../../../actions/server/builtin_action_types/swimlane/types'; export type SwimlaneActionConnector = UserConfiguredActionConnector< SwimlaneConfig, @@ -21,16 +25,8 @@ export interface SwimlaneConfig { mappings: SwimlaneMappingConfig; } -export interface SwimlaneMappingConfig { - alertIdConfig: SwimlaneFieldMappingConfig; - severityConfig: SwimlaneFieldMappingConfig; - caseNameConfig: SwimlaneFieldMappingConfig; - caseIdConfig: SwimlaneFieldMappingConfig; - ruleNameConfig: SwimlaneFieldMappingConfig; - commentsConfig: SwimlaneFieldMappingConfig; - descriptionConfig: SwimlaneFieldMappingConfig; - [key: string]: SwimlaneFieldMappingConfig; -} +export type MappingConfigurationKeys = keyof MappingConfigType; +export type SwimlaneMappingConfig = Record; export interface SwimlaneFieldMappingConfig { id: string; From c129d22bf5ea6632905061e82226a6f1d9393349 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 23 Jun 2021 10:57:50 +0300 Subject: [PATCH 93/96] Fix readme --- x-pack/plugins/actions/README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index f6676718411f4..b19e89a599840 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -56,7 +56,7 @@ Table of Contents - [`subActionParams (severity)`](#subactionparams-severity) - [Swimlane](#swimlane) - [`params`](#params-3) - - [| severity | The severity of the incident. | string _(optional)_ |](#-severity-----the-severity-of-the-incident------string-optional-) + - [| severity | The severity of the incident. | string _(optional)_ |](#-severity-----the-severity-of-the-incident-----string-optional-) - [Command Line Utility](#command-line-utility) - [Developing New Action Types](#developing-new-action-types) - [licensing](#licensing) @@ -419,14 +419,14 @@ No parameters for the `severity` subaction. Provide an empty object `{}`. The following table describes the properties of the `incident` object. -| Property | Description | Type | -| ----------- | --------------------------------- | ------------------- | -| alertName | The alert name of the incident. | string _(optional)_ | -| alertSource | The alert source of the incident. | string _(optional)_ | -| caseId | The case id of the incident. | string _(optional)_ | -| caseName | The case name of the incident. | string _(optional)_ | -| description | The description of the incident. | string _(optional)_ | -| severity | The severity of the incident. | string _(optional)_ | +| Property | Description | Type | +| ----------- | -------------------------------- | ------------------- | +| alertId | The alert id. | string _(optional)_ | +| caseId | The case id of the incident. | string _(optional)_ | +| caseName | The case name of the incident. | string _(optional)_ | +| description | The description of the incident. | string _(optional)_ | +| ruleName | The rule name. | string _(optional)_ | +| severity | The severity of the incident. | string _(optional)_ | --- # Command Line Utility From 3d7d89416aaf7e3b572e2f83d130bf844f03fc02 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 23 Jun 2021 11:21:47 +0300 Subject: [PATCH 94/96] Add more tests for helpers --- .../swimlane/helpers.test.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts index 423b3dcb3fb64..c2974ec28486c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts @@ -25,6 +25,7 @@ describe('Create Record Mapping', () => { const data = getBodyForEventAction(appId, mappings, params); expect(data.applicationId).toEqual(appId); expect(data.id).not.toBeDefined(); + expect(data.values?.[mappings.alertIdConfig?.id ?? 0]).toEqual(params.alertId); expect(data.values?.[mappings.ruleNameConfig.id]).toEqual(params.ruleName); expect(data.values?.[mappings.caseNameConfig?.id ?? 0]).toEqual(params.caseName); expect(data.values?.[mappings.caseIdConfig?.id ?? 0]).toEqual(params.caseId); @@ -61,4 +62,29 @@ describe('Create Record Mapping', () => { const data = getBodyForEventAction(appId, { ...mappings, test: null }, params); expect(data.values?.test).not.toBeDefined(); }); + + test('it converts a numeric values correctly', () => { + const params = { + alertId: 'thisIsNotANumber', + ruleName: 'Rule Name', + severity: 'Critical', + caseName: 'Case Name', + caseId: '123', + description: 'case desc', + externalId: null, + }; + + const data = getBodyForEventAction( + appId, + { + ...mappings, + caseIdConfig: { ...mappings.caseIdConfig, fieldType: 'numeric' }, + alertIdConfig: { ...mappings.alertIdConfig, fieldType: 'numeric' }, + }, + params + ); + + expect(data.values?.[mappings.alertIdConfig?.id ?? 0]).toBe(0); + expect(data.values?.[mappings.caseIdConfig?.id ?? 0]).toBe(123); + }); }); From 72f62020153fb6942c96a03e48ea3fde313b81b3 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 23 Jun 2021 12:28:28 +0300 Subject: [PATCH 95/96] PR feedback --- .../server/builtin_action_types/swimlane/index.ts | 2 +- .../builtin_action_types/swimlane/service.test.ts | 14 +++++++------- .../components/connectors/swimlane/validator.ts | 2 +- .../swimlane/steps/swimlane_connection.tsx | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts index 77afe4a0f4f01..de5010436b6b3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts @@ -32,7 +32,7 @@ interface GetActionTypeParams { configurationUtilities: ActionsConfigurationUtilities; } -const supportedSubActions: string[] = ['application', 'createRecord', 'pushToService']; +const supportedSubActions: string[] = ['pushToService']; // action type definition export function getActionType( diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts index fd925132f9266..77f4686f8acd0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts @@ -55,6 +55,8 @@ describe('Swimlane Service', () => { alertId: 'Alert Id', }; + const url = config.apiUrl.slice(0, -1); + beforeAll(() => { service = createExternalService( { @@ -162,7 +164,7 @@ describe('Swimlane Service', () => { id: '123', title: 'title', pushedDate: '2021-06-01T17:29:51.092Z', - url: `${config.apiUrl.slice(0, -1)}/record/${config.appId}/123`, + url: `${url}/record/${config.appId}/123`, }); }); @@ -190,7 +192,7 @@ describe('Swimlane Service', () => { [mappings.alertIdConfig.id]: 'Alert Id', }, }, - url: `${config.apiUrl.slice(0, -1)}/api/app/${config.appId}/record`, + url: `${url}/api/app/${config.appId}/record`, method: 'post', configurationUtilities, }); @@ -229,7 +231,7 @@ describe('Swimlane Service', () => { id: '123', title: 'title', pushedDate: '2021-06-01T17:29:51.092Z', - url: `${config.apiUrl.slice(0, -1)}/record/${config.appId}/123`, + url: `${url}/record/${config.appId}/123`, }); }); @@ -259,7 +261,7 @@ describe('Swimlane Service', () => { [mappings.alertIdConfig.id]: 'Alert Id', }, }, - url: `${config.apiUrl.slice(0, -1)}/api/app/${config.appId}/record/${incidentId}`, + url: `${url}/api/app/${config.appId}/record/${incidentId}`, method: 'patch', configurationUtilities, }); @@ -324,9 +326,7 @@ describe('Swimlane Service', () => { isRichText: true, message: comment.comment, }, - url: `${config.apiUrl.slice(0, -1)}/api/app/${config.appId}/record/${incidentId}/${ - mappings.commentsConfig.id - }/comment`, + url: `${url}/api/app/${config.appId}/record/${incidentId}/${mappings.commentsConfig.id}/comment`, method: 'post', configurationUtilities, }); diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts index 703110dc9946a..35f14c2460d1c 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts @@ -17,7 +17,7 @@ const casesRequiredFields = [ ]; export const isAnyRequiredFieldNotSet = (mapping: Record | undefined) => - !casesRequiredFields.some((field) => mapping != null && mapping[field] != null); + casesRequiredFields.some((field) => mapping?.[field] == null); /** * The user can use either a connector of type cases or all. diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx index a3cbdc840e4fe..cd29037e3535f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx @@ -58,7 +58,7 @@ const SwimlaneConnectionComponent: React.FunctionComponent = ({ // fetch swimlane application configuration const application = await getApplication(); - if (application != null && application.fields) { + if (application?.fields) { const allFields = application.fields; updateFields(allFields); updateCurrentStep(2); From cf6f92722a86a808ef95c6755595b60edf85daec Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 23 Jun 2021 12:29:18 +0300 Subject: [PATCH 96/96] Hide error message with css --- .../public/components/connector_selector/form.tsx | 12 ++++++++++-- .../components/connectors/swimlane/validator.test.ts | 4 ++-- .../components/connectors/swimlane/validator.ts | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/cases/public/components/connector_selector/form.tsx b/x-pack/plugins/cases/public/components/connector_selector/form.tsx index 210334e93adb8..71a65ae030d9d 100644 --- a/x-pack/plugins/cases/public/components/connector_selector/form.tsx +++ b/x-pack/plugins/cases/public/components/connector_selector/form.tsx @@ -8,6 +8,7 @@ import React, { useCallback } from 'react'; import { isEmpty } from 'lodash/fp'; import { EuiFormRow } from '@elastic/eui'; +import styled from 'styled-components'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../common/shared_imports'; import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown'; @@ -24,6 +25,13 @@ interface ConnectorSelectorProps { handleChange?: (newValue: string) => void; hideConnectorServiceNowSir?: boolean; } + +const EuiFormRowWrapper = styled(EuiFormRow)` + .euiFormErrorText { + display: none; + } +`; + export const ConnectorSelector = ({ connectors, dataTestSubj, @@ -47,7 +55,7 @@ export const ConnectorSelector = ({ ); return isEdit ? ( - - + ) : null; }; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts index c565c766c0e77..552d988c26330 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts @@ -29,7 +29,7 @@ describe('Swimlane validator', () => { mappings: {}, }, }; - expect(connectorValidator(invalidConnector)).toEqual({ message: '' }); + expect(connectorValidator(invalidConnector)).toEqual({ message: 'Invalid connector' }); }); test('it returns an error message if the connector is of type alerts', () => { @@ -40,7 +40,7 @@ describe('Swimlane validator', () => { connectorType: SwimlaneConnectorType.Alerts, }, }; - expect(connectorValidator(invalidConnector)).toEqual({ message: '' }); + expect(connectorValidator(invalidConnector)).toEqual({ message: 'Invalid connector' }); }); test.each([SwimlaneConnectorType.Cases, SwimlaneConnectorType.All])( diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts index 35f14c2460d1c..4ead75e5854f9 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts @@ -33,7 +33,7 @@ export const connectorValidator = ( } = connector; if (connectorType === SwimlaneConnectorType.Alerts || isAnyRequiredFieldNotSet(mappings)) { return { - message: '', + message: 'Invalid connector', }; } };

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 literal 168149 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�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