diff --git a/x-pack/plugins/case/common/api/cases/configure.ts b/x-pack/plugins/case/common/api/cases/configure.ts index d92af587d0e92b..7d20011a428cfe 100644 --- a/x-pack/plugins/case/common/api/cases/configure.ts +++ b/x-pack/plugins/case/common/api/cases/configure.ts @@ -8,10 +8,12 @@ import * as rt from 'io-ts'; import { ActionResult } from '../../../../actions/common'; import { UserRT } from '../user'; +import { JiraFieldsRT } from '../connectors/jira'; +import { ServiceNowFieldsRT } from '../connectors/servicenow'; /* * This types below are related to the service now configuration - * mapping between our case and service-now + * mapping between our case and [service-now, jira] * */ @@ -27,12 +29,7 @@ const CaseFieldRT = rt.union([ rt.literal('comments'), ]); -const ThirdPartyFieldRT = rt.union([ - rt.literal('comments'), - rt.literal('description'), - rt.literal('not_mapped'), - rt.literal('short_description'), -]); +const ThirdPartyFieldRT = rt.union([JiraFieldsRT, ServiceNowFieldsRT, rt.literal('not_mapped')]); export const CasesConfigurationMapsRT = rt.type({ source: CaseFieldRT, diff --git a/x-pack/plugins/case/common/api/connectors/index.ts b/x-pack/plugins/case/common/api/connectors/index.ts new file mode 100644 index 00000000000000..c1fc284c938b75 --- /dev/null +++ b/x-pack/plugins/case/common/api/connectors/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './jira'; +export * from './servicenow'; diff --git a/x-pack/plugins/case/common/api/connectors/jira.ts b/x-pack/plugins/case/common/api/connectors/jira.ts new file mode 100644 index 00000000000000..4e4674318ddd87 --- /dev/null +++ b/x-pack/plugins/case/common/api/connectors/jira.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const JiraFieldsRT = rt.union([ + rt.literal('summary'), + rt.literal('description'), + rt.literal('comments'), +]); + +export type JiraFieldsType = rt.TypeOf; diff --git a/x-pack/plugins/case/common/api/connectors/servicenow.ts b/x-pack/plugins/case/common/api/connectors/servicenow.ts new file mode 100644 index 00000000000000..fc124bfd460941 --- /dev/null +++ b/x-pack/plugins/case/common/api/connectors/servicenow.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const ServiceNowFieldsRT = rt.union([ + rt.literal('short_description'), + rt.literal('description'), + rt.literal('comments'), +]); + +export type ServiceNowFieldsType = rt.TypeOf; diff --git a/x-pack/plugins/siem/public/containers/case/configure/mock.ts b/x-pack/plugins/siem/public/containers/case/configure/mock.ts index c6824bd50edb5c..88e1793aa15c1e 100644 --- a/x-pack/plugins/siem/public/containers/case/configure/mock.ts +++ b/x-pack/plugins/siem/public/containers/case/configure/mock.ts @@ -30,7 +30,7 @@ export const mapping: CasesConfigurationMapping[] = [ ]; export const connectorsMock: Connector[] = [ { - id: '123', + id: 'servicenow-1', actionTypeId: '.servicenow', name: 'My Connector', config: { @@ -42,7 +42,7 @@ export const connectorsMock: Connector[] = [ isPreconfigured: false, }, { - id: '456', + id: 'servicenow-2', actionTypeId: '.servicenow', name: 'My Connector 2', config: { @@ -69,6 +69,34 @@ export const connectorsMock: Connector[] = [ }, isPreconfigured: false, }, + { + id: 'jira-1', + actionTypeId: '.jira', + name: 'Jira', + config: { + apiUrl: 'https://instance.atlassian.ne', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'summary', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, + isPreconfigured: false, + }, ]; export const caseConfigurationResposeMock: CasesConfigureResponse = { diff --git a/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx b/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx index c5a35da56284d9..10b1e75c6ea845 100644 --- a/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx +++ b/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx @@ -13,14 +13,16 @@ import { isEmpty, get } from 'lodash/fp'; import { ActionConnectorFieldsProps } from '../../../../../../triggers_actions_ui/public/types'; import { FieldMapping } from '../../../../pages/case/components/configure_cases/field_mapping'; -import { defaultMapping } from '../../config'; import { CasesConfigurationMapping } from '../../../../containers/case/configure/types'; import * as i18n from '../../translations'; import { ActionConnector, ConnectorFlyoutHOCProps } from '../../types'; +import { createDefaultMapping } from '../../utils'; +import { connectorsConfiguration } from '../../config'; export const withConnectorFlyout = ({ ConnectorFormComponent, + connectorActionTypeId, secretKeys = [], configKeys = [], }: ConnectorFlyoutHOCProps) => { @@ -56,7 +58,7 @@ export const withConnectorFlyout = ({ if (isEmpty(mapping)) { editActionConfig('casesConfiguration', { ...action.config.casesConfiguration, - mapping: defaultMapping, + mapping: createDefaultMapping(connectorsConfiguration[connectorActionTypeId].fields), }); } @@ -135,6 +137,7 @@ export const withConnectorFlyout = ({ diff --git a/x-pack/plugins/siem/public/lib/connectors/config.ts b/x-pack/plugins/siem/public/lib/connectors/config.ts index 98473e49622a95..d8b55665f77683 100644 --- a/x-pack/plugins/siem/public/lib/connectors/config.ts +++ b/x-pack/plugins/siem/public/lib/connectors/config.ts @@ -4,31 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CasesConfigurationMapping } from '../../containers/case/configure/types'; - -import { Connector } from './types'; import { connector as serviceNowConnectorConfig } from './servicenow/config'; import { connector as jiraConnectorConfig } from './jira/config'; +import { ConnectorConfiguration } from './types'; -export const connectorsConfiguration: Record = { +export const connectorsConfiguration: Record = { '.servicenow': serviceNowConnectorConfig, '.jira': jiraConnectorConfig, }; - -export const defaultMapping: CasesConfigurationMapping[] = [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, -]; diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/config.ts b/x-pack/plugins/siem/public/lib/connectors/jira/config.ts index 42bd1b9cdc1915..e6151a54bff74f 100644 --- a/x-pack/plugins/siem/public/lib/connectors/jira/config.ts +++ b/x-pack/plugins/siem/public/lib/connectors/jira/config.ts @@ -4,17 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Connector } from '../types'; +import { ConnectorConfiguration } from './types'; -import { JIRA_TITLE } from './translations'; +import * as i18n from './translations'; import logo from './logo.svg'; -export const connector: Connector = { +export const connector: ConnectorConfiguration = { id: '.jira', - name: JIRA_TITLE, + name: i18n.JIRA_TITLE, logo, enabled: true, enabledInConfig: true, enabledInLicense: true, minimumLicenseRequired: 'platinum', + fields: { + summary: { + label: i18n.MAPPING_FIELD_SUMMARY, + validSourceFields: ['title', 'description'], + defaultSourceField: 'title', + defaultActionType: 'overwrite', + }, + description: { + label: i18n.MAPPING_FIELD_DESC, + validSourceFields: ['title', 'description'], + defaultSourceField: 'description', + defaultActionType: 'overwrite', + }, + comments: { + label: i18n.MAPPING_FIELD_COMMENTS, + validSourceFields: ['comments'], + defaultSourceField: 'comments', + defaultActionType: 'append', + }, + }, }; diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx b/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx index 482808fca53b1a..9c3d1c90e67d7a 100644 --- a/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx +++ b/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx @@ -107,4 +107,5 @@ export const JiraConnectorFlyout = withConnectorFlyout({ ConnectorFormComponent: JiraConnectorForm, secretKeys: ['email', 'apiToken'], configKeys: ['projectKey'], + connectorActionTypeId: '.jira', }); diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/logo.svg b/x-pack/plugins/siem/public/lib/connectors/jira/logo.svg index dcd022a8dca18d..8560cf7e270c89 100644 --- a/x-pack/plugins/siem/public/lib/connectors/jira/logo.svg +++ b/x-pack/plugins/siem/public/lib/connectors/jira/logo.svg @@ -1,5 +1,9 @@ - - - - + + + + + + + + diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/translations.ts b/x-pack/plugins/siem/public/lib/connectors/jira/translations.ts index 751aaecdad9641..f95663d4026047 100644 --- a/x-pack/plugins/siem/public/lib/connectors/jira/translations.ts +++ b/x-pack/plugins/siem/public/lib/connectors/jira/translations.ts @@ -26,3 +26,10 @@ export const JIRA_PROJECT_KEY_REQUIRED = i18n.translate( defaultMessage: 'Project key is required', } ); + +export const MAPPING_FIELD_SUMMARY = i18n.translate( + 'xpack.siem.case.configureCases.mappingFieldSummary', + { + defaultMessage: 'Summary', + } +); diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/types.ts b/x-pack/plugins/siem/public/lib/connectors/jira/types.ts index 13e4e8f6a289ef..d6b8a6cadcb902 100644 --- a/x-pack/plugins/siem/public/lib/connectors/jira/types.ts +++ b/x-pack/plugins/siem/public/lib/connectors/jira/types.ts @@ -12,6 +12,10 @@ import { JiraSecretConfigurationType, } from '../../../../../actions/server/builtin_action_types/jira/types'; +export { JiraFieldsType } from '../../../../../case/common/api/connectors'; + +export * from '../types'; + export interface JiraActionConnector { config: JiraPublicConfigurationType; secrets: JiraSecretConfigurationType; diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/config.ts b/x-pack/plugins/siem/public/lib/connectors/servicenow/config.ts index 7bc1b117b34226..35c677c9574e36 100644 --- a/x-pack/plugins/siem/public/lib/connectors/servicenow/config.ts +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/config.ts @@ -4,17 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Connector } from '../types'; - -import { SERVICENOW_TITLE } from './translations'; +import { ConnectorConfiguration } from './types'; +import * as i18n from './translations'; import logo from './logo.svg'; -export const connector: Connector = { +export const connector: ConnectorConfiguration = { id: '.servicenow', - name: SERVICENOW_TITLE, + name: i18n.SERVICENOW_TITLE, logo, enabled: true, enabledInConfig: true, enabledInLicense: true, minimumLicenseRequired: 'platinum', + fields: { + short_description: { + label: i18n.MAPPING_FIELD_SHORT_DESC, + validSourceFields: ['title', 'description'], + defaultSourceField: 'title', + defaultActionType: 'overwrite', + }, + description: { + label: i18n.MAPPING_FIELD_DESC, + validSourceFields: ['title', 'description'], + defaultSourceField: 'description', + defaultActionType: 'overwrite', + }, + comments: { + label: i18n.MAPPING_FIELD_COMMENTS, + validSourceFields: ['comments'], + defaultSourceField: 'comments', + defaultActionType: 'append', + }, + }, }; diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx b/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx index bcde802e7bd1e0..5d5d08dacf90c9 100644 --- a/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx @@ -80,4 +80,5 @@ const ServiceNowConnectorForm: React.FC({ ConnectorFormComponent: ServiceNowConnectorForm, secretKeys: ['username', 'password'], + connectorActionTypeId: '.servicenow', }); diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/translations.ts b/x-pack/plugins/siem/public/lib/connectors/servicenow/translations.ts index 5dac9eddd15369..39d0ee96513a25 100644 --- a/x-pack/plugins/siem/public/lib/connectors/servicenow/translations.ts +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/translations.ts @@ -21,3 +21,10 @@ export const SERVICENOW_TITLE = i18n.translate( defaultMessage: 'ServiceNow', } ); + +export const MAPPING_FIELD_SHORT_DESC = i18n.translate( + 'xpack.siem.case.configureCases.mappingFieldShortDescription', + { + defaultMessage: 'Short Description', + } +); diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts b/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts index b7f0e79eb37e34..43da5624a497b4 100644 --- a/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts @@ -12,6 +12,10 @@ import { ServiceNowSecretConfigurationType, } from '../../../../../actions/server/builtin_action_types/servicenow/types'; +export { ServiceNowFieldsType } from '../../../../../case/common/api/connectors'; + +export * from '../types'; + export interface ServiceNowActionConnector { config: ServiceNowPublicConfigurationType; secrets: ServiceNowSecretConfigurationType; diff --git a/x-pack/plugins/siem/public/lib/connectors/translations.ts b/x-pack/plugins/siem/public/lib/connectors/translations.ts index b9c1d0fa2a17fa..071fd8ef12645f 100644 --- a/x-pack/plugins/siem/public/lib/connectors/translations.ts +++ b/x-pack/plugins/siem/public/lib/connectors/translations.ts @@ -79,3 +79,17 @@ export const EMAIL_REQUIRED = i18n.translate( defaultMessage: 'Email is required', } ); + +export const MAPPING_FIELD_DESC = i18n.translate( + 'xpack.siem.case.configureCases.mappingFieldDescription', + { + defaultMessage: 'Description', + } +); + +export const MAPPING_FIELD_COMMENTS = i18n.translate( + 'xpack.siem.case.configureCases.mappingFieldComments', + { + defaultMessage: 'Comments', + } +); diff --git a/x-pack/plugins/siem/public/lib/connectors/types.ts b/x-pack/plugins/siem/public/lib/connectors/types.ts index 9af60f4995e54c..ffb013c347e59e 100644 --- a/x-pack/plugins/siem/public/lib/connectors/types.ts +++ b/x-pack/plugins/siem/public/lib/connectors/types.ts @@ -10,8 +10,20 @@ import { ActionType } from '../../../../triggers_actions_ui/public'; import { ExternalIncidentServiceConfiguration } from '../../../../actions/server/builtin_action_types/case/types'; -export interface Connector extends ActionType { +import { ActionType as ThirdPartySupportedActions, CaseField } from '../../../../case/common/api'; + +export { ThirdPartyField as AllThirdPartyFields } from '../../../../case/common/api'; + +export interface ThirdPartyField { + label: string; + validSourceFields: CaseField[]; + defaultSourceField: CaseField; + defaultActionType: ThirdPartySupportedActions; +} + +export interface ConnectorConfiguration extends ActionType { logo: string; + fields: Record; } export interface ActionConnector { @@ -40,6 +52,7 @@ export interface ConnectorFlyoutFormProps { export interface ConnectorFlyoutHOCProps { ConnectorFormComponent: React.FC>; + connectorActionTypeId: string; configKeys?: string[]; secretKeys?: string[]; } diff --git a/x-pack/plugins/siem/public/lib/connectors/utils.ts b/x-pack/plugins/siem/public/lib/connectors/utils.ts index 5b5270ade5a654..169b4758876e8c 100644 --- a/x-pack/plugins/siem/public/lib/connectors/utils.ts +++ b/x-pack/plugins/siem/public/lib/connectors/utils.ts @@ -16,10 +16,12 @@ import { ActionConnectorParams, ActionConnectorValidationErrors, Optional, + ThirdPartyField, } from './types'; import { isUrlInvalid } from './validators'; import * as i18n from './translations'; +import { CasesConfigurationMapping } from '../../containers/case/configure/types'; export const createActionType = ({ id, @@ -69,3 +71,15 @@ const ConnectorParamsFields: React.FunctionComponent { return { errors: {} }; }; + +export const createDefaultMapping = ( + fields: Record +): CasesConfigurationMapping[] => + Object.keys(fields).map( + key => + ({ + source: fields[key].defaultSourceField, + target: key, + actionType: fields[key].defaultActionType, + } as CasesConfigurationMapping) + ); diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx index 209dce9aedffc5..eaef524b13da86 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx @@ -61,7 +61,6 @@ describe('ClosureOptions', () => { test('the closure type is changed successfully', () => { wrapper.find('input[id="close-by-pushing"]').simulate('change'); - expect(onChangeClosureType).toHaveBeenCalled(); expect(onChangeClosureType).toHaveBeenCalledWith('close-by-pushing'); }); }); diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx index 5fb52c374b482b..125a42b1264667 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx @@ -69,15 +69,15 @@ describe('Connectors', () => { test('the connector is changed successfully', () => { wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); + wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); expect(onChangeConnector).toHaveBeenCalled(); - expect(onChangeConnector).toHaveBeenCalledWith('456'); + expect(onChangeConnector).toHaveBeenCalledWith('servicenow-2'); }); test('the connector is changed successfully to none', () => { onChangeConnector.mockClear(); - const newWrapper = mount(, { + const newWrapper = mount(, { wrappingComponent: TestProviders, }); diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx index 044108962efc7b..6abe4f1ac00adb 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx @@ -44,8 +44,14 @@ describe('ConnectorsDropdown', () => { value: 'none', 'data-test-subj': 'dropdown-connector-no-connector', }), - expect.objectContaining({ value: '123', 'data-test-subj': 'dropdown-connector-123' }), - expect.objectContaining({ value: '456', 'data-test-subj': 'dropdown-connector-456' }), + expect.objectContaining({ + value: 'servicenow-1', + 'data-test-subj': 'dropdown-connector-servicenow-1', + }), + expect.objectContaining({ + value: 'servicenow-2', + 'data-test-subj': 'dropdown-connector-servicenow-2', + }), ]) ); }); @@ -77,7 +83,7 @@ describe('ConnectorsDropdown', () => { }); test('it selects the correct connector', () => { - const newWrapper = mount(, { + const newWrapper = mount(, { wrappingComponent: TestProviders, }); diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx index 9ab752bb589c0a..498757a34b78d2 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx @@ -7,10 +7,12 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; +import { connectorsConfiguration } from '../../../../lib/connectors/config'; +import { createDefaultMapping } from '../../../../lib/connectors/utils'; + import { FieldMapping, FieldMappingProps } from './field_mapping'; import { mapping } from './__mock__'; import { FieldMappingRow } from './field_mapping_row'; -import { defaultMapping } from '../../../../lib/connectors/config'; import { TestProviders } from '../../../../mock'; describe('FieldMappingRow', () => { @@ -20,6 +22,7 @@ describe('FieldMappingRow', () => { disabled: false, mapping, onChangeMapping, + connectorActionTypeId: '.servicenow', }; beforeAll(() => { @@ -66,6 +69,9 @@ describe('FieldMappingRow', () => { wrappingComponent: TestProviders, }); + const selectedConnector = connectorsConfiguration['.servicenow']; + const defaultMapping = createDefaultMapping(selectedConnector.fields); + const rows = newWrapper.find(FieldMappingRow); rows.forEach((row, index) => { expect(row.prop('siemField')).toEqual(defaultMapping[index].source); diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx index 2934b1056e29cd..41a6fbca3c0072 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx @@ -4,53 +4,83 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiFormRow, EuiFlexItem, EuiFlexGroup, EuiSuperSelectOption } from '@elastic/eui'; import styled from 'styled-components'; import { CasesConfigurationMapping, - ThirdPartyField, CaseField, ActionType, + ThirdPartyField, } from '../../../../containers/case/configure/types'; import { FieldMappingRow } from './field_mapping_row'; import * as i18n from './translations'; -import { defaultMapping } from '../../../../lib/connectors/config'; +import { connectorsConfiguration } from '../../../../lib/connectors/config'; import { setActionTypeToMapping, setThirdPartyToMapping } from './utils'; +import { + ThirdPartyField as ConnectorConfigurationThirdPartyField, + AllThirdPartyFields, +} from '../../../../lib/connectors/types'; +import { createDefaultMapping } from '../../../../lib/connectors/utils'; const FieldRowWrapper = styled.div` margin-top: 8px; font-size: 14px; `; -const supportedThirdPartyFields: Array> = [ - { - value: 'not_mapped', - inputDisplay: {i18n.FIELD_MAPPING_FIELD_NOT_MAPPED}, - 'data-test-subj': 'third-party-field-not-mapped', - }, +const actionTypeOptions: Array> = [ { - value: 'short_description', - inputDisplay: {i18n.FIELD_MAPPING_FIELD_SHORT_DESC}, - 'data-test-subj': 'third-party-field-short-description', + value: 'nothing', + inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_NOTHING}, + 'data-test-subj': 'edit-update-option-nothing', }, { - value: 'comments', - inputDisplay: {i18n.FIELD_MAPPING_FIELD_COMMENTS}, - 'data-test-subj': 'third-party-field-comments', + value: 'overwrite', + inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_OVERWRITE}, + 'data-test-subj': 'edit-update-option-overwrite', }, { - value: 'description', - inputDisplay: {i18n.FIELD_MAPPING_FIELD_DESC}, - 'data-test-subj': 'third-party-field-description', + value: 'append', + inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_APPEND}, + 'data-test-subj': 'edit-update-option-append', }, ]; +const getThirdPartyOptions = ( + caseField: CaseField, + thirdPartyFields: Record +): Array> => + (Object.keys(thirdPartyFields) as AllThirdPartyFields[]).reduce< + Array> + >( + (acc, key) => { + if (thirdPartyFields[key].validSourceFields.includes(caseField)) { + return [ + ...acc, + { + value: key, + inputDisplay: {thirdPartyFields[key].label}, + 'data-test-subj': `dropdown-mapping-${key}`, + }, + ]; + } + return acc; + }, + [ + { + value: 'not_mapped', + inputDisplay: i18n.MAPPING_FIELD_NOT_MAPPED, + 'data-test-subj': 'dropdown-mapping-not_mapped', + }, + ] + ); + export interface FieldMappingProps { disabled: boolean; mapping: CasesConfigurationMapping[] | null; + connectorActionTypeId: string; onChangeMapping: (newMapping: CasesConfigurationMapping[]) => void; } @@ -58,6 +88,7 @@ const FieldMappingComponent: React.FC = ({ disabled, mapping, onChangeMapping, + connectorActionTypeId, }) => { const onChangeActionType = useCallback( (caseField: CaseField, newActionType: ActionType) => { @@ -74,6 +105,12 @@ const FieldMappingComponent: React.FC = ({ }, [mapping] ); + + const selectedConnector = connectorsConfiguration[connectorActionTypeId] ?? { fields: {} }; + const defaultMapping = useMemo(() => createDefaultMapping(selectedConnector.fields), [ + selectedConnector.fields, + ]); + return ( <> @@ -92,10 +129,12 @@ const FieldMappingComponent: React.FC = ({ {(mapping ?? defaultMapping).map(item => ( > = [ { @@ -25,15 +25,35 @@ const thirdPartyOptions: Array> = [ }, ]; +const actionTypeOptions: Array> = [ + { + value: 'nothing', + inputDisplay: <>{'Nothing'}, + 'data-test-subj': 'edit-update-option-nothing', + }, + { + value: 'overwrite', + inputDisplay: <>{'Overwrite'}, + 'data-test-subj': 'edit-update-option-overwrite', + }, + { + value: 'append', + inputDisplay: <>{'Append'}, + 'data-test-subj': 'edit-update-option-append', + }, +]; + describe('FieldMappingRow', () => { let wrapper: ReactWrapper; const onChangeActionType = jest.fn(); const onChangeThirdParty = jest.fn(); const props: RowProps = { + id: 'title', disabled: false, siemField: 'title', thirdPartyOptions, + actionTypeOptions, onChangeActionType, onChangeThirdParty, selectedActionType: 'nothing', @@ -47,14 +67,14 @@ describe('FieldMappingRow', () => { test('it renders', () => { expect( wrapper - .find('[data-test-subj="case-configure-third-party-select"]') + .find('[data-test-subj="case-configure-third-party-select-title"]') .first() .exists() ).toBe(true); expect( wrapper - .find('[data-test-subj="case-configure-action-type-select"]') + .find('[data-test-subj="case-configure-action-type-select-title"]') .first() .exists() ).toBe(true); diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx index 732a11a58d35a7..687b0517326eb0 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx @@ -14,45 +14,32 @@ import { } from '@elastic/eui'; import { capitalize } from 'lodash/fp'; -import * as i18n from './translations'; + import { CaseField, ActionType, ThirdPartyField, } from '../../../../containers/case/configure/types'; +import { AllThirdPartyFields } from '../../../../lib/connectors/types'; export interface RowProps { + id: string; disabled: boolean; siemField: CaseField; - thirdPartyOptions: Array>; + thirdPartyOptions: Array>; + actionTypeOptions: Array>; onChangeActionType: (caseField: CaseField, newActionType: ActionType) => void; onChangeThirdParty: (caseField: CaseField, newThirdPartyField: ThirdPartyField) => void; selectedActionType: ActionType; selectedThirdParty: ThirdPartyField; } -const actionTypeOptions: Array> = [ - { - value: 'nothing', - inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_NOTHING}, - 'data-test-subj': 'edit-update-option-nothing', - }, - { - value: 'overwrite', - inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_OVERWRITE}, - 'data-test-subj': 'edit-update-option-overwrite', - }, - { - value: 'append', - inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_APPEND}, - 'data-test-subj': 'edit-update-option-append', - }, -]; - const FieldMappingRowComponent: React.FC = ({ + id, disabled, siemField, thirdPartyOptions, + actionTypeOptions, onChangeActionType, onChangeThirdParty, selectedActionType, @@ -77,7 +64,7 @@ const FieldMappingRowComponent: React.FC = ({ options={thirdPartyOptions} valueOfSelected={selectedThirdParty} onChange={onChangeThirdParty.bind(null, siemField)} - data-test-subj={'case-configure-third-party-select'} + data-test-subj={`case-configure-third-party-select-${id}`} /> @@ -86,7 +73,7 @@ const FieldMappingRowComponent: React.FC = ({ options={actionTypeOptions} valueOfSelected={selectedActionType} onChange={onChangeActionType.bind(null, siemField)} - data-test-subj={'case-configure-action-type-select'} + data-test-subj={`case-configure-action-type-select-${id}`} /> diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx index fde179f3d25fc1..0359c1dbdba673 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx @@ -30,7 +30,6 @@ import { useCaseConfigureResponse, useConnectorsResponse, kibanaMockImplementationArgs, - mapping, } from './__mock__'; jest.mock('../../../../lib/kibana'); @@ -140,13 +139,13 @@ describe('ConfigureCases', () => { jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping, + mapping: connectors[0].config.casesConfiguration.mapping, closureType: 'close-by-user', - connectorId: '123', + connectorId: 'servicenow-1', connectorName: 'unchanged', currentConfiguration: { connectorName: 'unchanged', - connectorId: '123', + connectorId: 'servicenow-1', closureType: 'close-by-user', }, })); @@ -166,7 +165,7 @@ describe('ConfigureCases', () => { expect(wrapper.find(Connectors).prop('connectors')).toEqual(connectors); expect(wrapper.find(Connectors).prop('disabled')).toBe(false); expect(wrapper.find(Connectors).prop('isLoading')).toBe(false); - expect(wrapper.find(Connectors).prop('selectedConnector')).toBe('123'); + expect(wrapper.find(Connectors).prop('selectedConnector')).toBe('servicenow-1'); // ClosureOptions expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(false); @@ -175,6 +174,7 @@ describe('ConfigureCases', () => { // Mapping expect(wrapper.find(Mapping).prop('disabled')).toBe(true); expect(wrapper.find(Mapping).prop('updateConnectorDisabled')).toBe(false); + expect(wrapper.find(Mapping).prop('connectorActionTypeId')).toBe('.servicenow'); expect(wrapper.find(Mapping).prop('mapping')).toEqual( connectors[0].config.casesConfiguration.mapping ); @@ -182,24 +182,12 @@ describe('ConfigureCases', () => { // Flyouts expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(false); expect(wrapper.find(ConnectorAddFlyout).prop('actionTypes')).toEqual([ - { + expect.objectContaining({ id: '.servicenow', - name: 'ServiceNow', - enabled: true, - logo: 'test-file-stub', - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'platinum', - }, - { + }), + expect.objectContaining({ id: '.jira', - name: 'Jira', - logo: 'test-file-stub', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'platinum', - }, + }), ]); expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(false); @@ -223,8 +211,6 @@ describe('ConfigureCases', () => { }); // TODO: When mapping is enabled the test.todo should be implemented. - test.todo('the mapping is changed successfully when changing the third party'); - test.todo('the mapping is changed successfully when changing the action type'); test.todo('it disables the update connector button when loading the configuration'); test('it disables correctly when the user cannot crud', () => { @@ -274,13 +260,13 @@ describe('ConfigureCases', () => { test('it disables the buttons of action bar when loading connectors', () => { useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping, + mapping: connectors[1].config.casesConfiguration.mapping, closureType: 'close-by-user', - connectorId: '456', + connectorId: 'servicenow-2', connectorName: 'unchanged', currentConfiguration: { connectorName: 'unchanged', - connectorId: '123', + connectorId: 'servicenow-1', closureType: 'close-by-user', }, })); @@ -335,13 +321,13 @@ describe('ConfigureCases', () => { test('it disables the buttons of action bar when saving configuration', () => { useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping, + mapping: connectors[1].config.casesConfiguration.mapping, closureType: 'close-by-user', - connectorId: '456', + connectorId: 'servicenow-2', connectorName: 'unchanged', currentConfiguration: { connectorName: 'unchanged', - connectorId: '123', + connectorId: 'servicenow-1', closureType: 'close-by-user', }, persistLoading: true, @@ -369,13 +355,13 @@ describe('ConfigureCases', () => { test('it shows the loading spinner when saving configuration', () => { useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping, + mapping: connectors[1].config.casesConfiguration.mapping, closureType: 'close-by-user', - connectorId: '456', + connectorId: 'servicenow-2', connectorName: 'unchanged', currentConfiguration: { connectorName: 'unchanged', - connectorId: '123', + connectorId: 'servicenow-1', closureType: 'close-by-user', }, persistLoading: true, @@ -409,13 +395,13 @@ describe('ConfigureCases', () => { jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping, + mapping: connectors[1].config.casesConfiguration.mapping, closureType: 'close-by-user', - connectorId: '456', + connectorId: 'servicenow-2', connectorName: 'unchanged', currentConfiguration: { connectorName: 'unchanged', - connectorId: '123', + connectorId: 'servicenow-1', closureType: 'close-by-user', }, persistCaseConfigure, @@ -437,7 +423,7 @@ describe('ConfigureCases', () => { expect(persistCaseConfigure).toHaveBeenCalled(); expect(persistCaseConfigure).toHaveBeenCalledWith({ - connectorId: '456', + connectorId: 'servicenow-2', connectorName: 'My Connector 2', closureType: 'close-by-user', }); @@ -451,16 +437,17 @@ describe('ConfigureCases', () => { .prop('href') ).toBe(`#/link-to/case${searchURL}`); }); + test('it disables the buttons of action bar when loading configuration', () => { useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping, + mapping: connectors[1].config.casesConfiguration.mapping, closureType: 'close-by-user', - connectorId: '456', + connectorId: 'servicenow-2', connectorName: 'unchanged', currentConfiguration: { connectorName: 'unchanged', - connectorId: '123', + connectorId: 'servicenow-1', closureType: 'close-by-user', }, loading: true, @@ -490,13 +477,13 @@ describe('ConfigureCases', () => { jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping, + mapping: connectors[1].config.casesConfiguration.mapping, closureType: 'close-by-user', - connectorId: '456', + connectorId: 'servicenow-2', connectorName: 'unchanged', currentConfiguration: { connectorName: 'unchanged', - connectorId: '456', + connectorId: 'servicenow-2', closureType: 'close-by-user', }, })); @@ -530,20 +517,20 @@ describe('ConfigureCases', () => { test('it tracks the changes successfully', () => { useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping, + mapping: connectors[1].config.casesConfiguration.mapping, closureType: 'close-by-user', - connectorId: '456', + connectorId: 'servicenow-2', connectorName: 'unchanged', currentConfiguration: { connectorName: 'unchanged', - connectorId: '123', + connectorId: 'servicenow-1', closureType: 'close-by-pushing', }, })); const wrapper = mount(, { wrappingComponent: TestProviders }); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); + wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); wrapper.update(); wrapper.find('input[id="close-by-pushing"]').simulate('change'); wrapper.update(); @@ -558,23 +545,25 @@ describe('ConfigureCases', () => { .text() ).toBe('2 unsaved changes'); }); + test('it tracks the changes successfully when name changes', () => { useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping, + mapping: connectors[1].config.casesConfiguration.mapping, closureType: 'close-by-user', - connectorId: '456', + connectorId: 'servicenow-2', connectorName: 'nameChange', currentConfiguration: { - connectorId: '123', + connectorId: 'servicenow-1', closureType: 'close-by-pushing', connectorName: 'before', }, })); + const wrapper = mount(, { wrappingComponent: TestProviders }); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); + wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); wrapper.update(); wrapper.find('input[id="close-by-pushing"]').simulate('change'); wrapper.update(); @@ -595,7 +584,7 @@ describe('ConfigureCases', () => { // change settings wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); + wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); wrapper.update(); wrapper.find('input[id="close-by-pushing"]').simulate('change'); wrapper.update(); @@ -603,7 +592,7 @@ describe('ConfigureCases', () => { // revert back to initial settings wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-123"]').simulate('click'); + wrapper.find('button[data-test-subj="dropdown-connector-servicenow-1"]').simulate('click'); wrapper.update(); wrapper.find('input[id="close-by-user"]').simulate('change'); wrapper.update(); @@ -617,17 +606,17 @@ describe('ConfigureCases', () => { useCaseConfigureMock .mockImplementationOnce(() => ({ ...useCaseConfigureResponse, - mapping, + mapping: connectors[1].config.casesConfiguration.mapping, closureType: 'close-by-user', - connectorId: '456', - currentConfiguration: { connectorId: '456', closureType: 'close-by-user' }, + connectorId: 'servicenow-2', + currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, })) .mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping, + mapping: connectors[1].config.casesConfiguration.mapping, closureType: 'close-by-pushing', - connectorId: '456', - currentConfiguration: { connectorId: '456', closureType: 'close-by-user' }, + connectorId: 'servicenow-2', + currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, })); const wrapper = mount(, { wrappingComponent: TestProviders }); // Change closure type @@ -672,17 +661,17 @@ describe('ConfigureCases', () => { useCaseConfigureMock .mockImplementationOnce(() => ({ ...useCaseConfigureResponse, - mapping, + mapping: connectors[1].config.casesConfiguration.mapping, closureType: 'close-by-user', - connectorId: '456', - currentConfiguration: { connectorId: '456', closureType: 'close-by-user' }, + connectorId: 'servicenow-2', + currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, })) .mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping, + mapping: connectors[1].config.casesConfiguration.mapping, closureType: 'close-by-pushing', - connectorId: '456', - currentConfiguration: { connectorId: '456', closureType: 'close-by-user' }, + connectorId: 'servicenow-2', + currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, })); const wrapper = mount(, { wrappingComponent: TestProviders }); @@ -724,22 +713,22 @@ describe('ConfigureCases', () => { useCaseConfigureMock .mockImplementationOnce(() => ({ ...useCaseConfigureResponse, - mapping, + mapping: connectors[0].config.casesConfiguration.mapping, closureType: 'close-by-user', - connectorId: '123', - currentConfiguration: { connectorId: '123', closureType: 'close-by-user' }, + connectorId: 'servicenow-1', + currentConfiguration: { connectorId: 'servicenow-1', closureType: 'close-by-user' }, })) .mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping, + mapping: connectors[1].config.casesConfiguration.mapping, closureType: 'close-by-user', - connectorId: '456', - currentConfiguration: { connectorId: '123', closureType: 'close-by-user' }, + connectorId: 'servicenow-2', + currentConfiguration: { connectorId: 'servicenow-1', closureType: 'close-by-user' }, })); const wrapper = mount(, { wrappingComponent: TestProviders }); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); + wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); wrapper.update(); expect( @@ -757,17 +746,17 @@ describe('ConfigureCases', () => { useCaseConfigureMock .mockImplementationOnce(() => ({ ...useCaseConfigureResponse, - mapping, + mapping: connectors[1].config.casesConfiguration.mapping, closureType: 'close-by-user', - connectorId: '456', - currentConfiguration: { connectorId: '456', closureType: 'close-by-user' }, + connectorId: 'servicenow-2', + currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, })) .mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping, + mapping: connectors[1].config.casesConfiguration.mapping, closureType: 'close-by-pushing', - connectorId: '456', - currentConfiguration: { connectorId: '456', closureType: 'close-by-user' }, + connectorId: 'servicenow-2', + currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, })); const wrapper = mount(, { wrappingComponent: TestProviders }); wrapper.find('input[id="close-by-pushing"]').simulate('change'); @@ -788,5 +777,30 @@ describe('ConfigureCases', () => { wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() ).toBeFalsy(); }); + + test('it sets the mapping correctly when changing connector types', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[2].config.casesConfiguration.mapping, + closureType: 'close-by-user', + connectorId: 'jira-1', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: 'servicenow-1', + closureType: 'close-by-user', + }, + persistLoading: false, + })); + + const wrapper = mount(, { wrappingComponent: TestProviders }); + expect( + wrapper.find('button[data-test-subj="case-configure-third-party-select-title"]').text() + ).toBe('Summary'); + }); + + // TODO: When mapping is enabled the test.todo should be implemented. + test.todo('the mapping is changed successfully when changing the third party'); + test.todo('the mapping is changed successfully when changing the action type'); }); }); diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx index 18861db9a92053..40def5231a3042 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useState, Dispatch, SetStateAction } from 'react'; +import React, { useCallback, useEffect, useState, Dispatch, SetStateAction, useMemo } from 'react'; import styled, { css } from 'styled-components'; import { @@ -200,6 +200,11 @@ const ConfigureCasesComponent: React.FC = ({ userC currentConfiguration.closureType, ]); + const connectorActionTypeId = useMemo( + () => connectors.find(c => c.id === connectorId)?.actionTypeId ?? '.none', + [connectorId, connectors] + ); + return ( {!connectorIsValid && ( @@ -236,6 +241,7 @@ const ConfigureCasesComponent: React.FC = ({ userC disabled updateConnectorDisabled={updateConnectorDisabled || !userCanCrud} mapping={mapping} + connectorActionTypeId={connectorActionTypeId} onChangeMapping={setMapping} setEditFlyoutVisibility={onClickUpdateConnector} /> diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx index fefcb2ca8cf6a3..083904d303490f 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx @@ -21,45 +21,264 @@ describe('Mapping', () => { updateConnectorDisabled: false, onChangeMapping, setEditFlyoutVisibility, + connectorActionTypeId: '.servicenow', }; - beforeAll(() => { + beforeEach(() => { + jest.clearAllMocks(); wrapper = mount(, { wrappingComponent: TestProviders }); }); - test('it shows mapping form group', () => { - expect( - wrapper - .find('[data-test-subj="case-mapping-form-group"]') - .first() - .exists() - ).toBe(true); + afterEach(() => { + wrapper.unmount(); }); - test('it shows mapping form row', () => { - expect( + describe('Common', () => { + test('it shows mapping form group', () => { + expect( + wrapper + .find('[data-test-subj="case-mapping-form-group"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows mapping form row', () => { + expect( + wrapper + .find('[data-test-subj="case-mapping-form-row"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows the update button', () => { + expect( + wrapper + .find('[data-test-subj="case-mapping-update-connector-button"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows the field mapping', () => { + expect( + wrapper + .find('[data-test-subj="case-mapping-field"]') + .first() + .exists() + ).toBe(true); + }); + + test('it updates thirdParty correctly', () => { wrapper - .find('[data-test-subj="case-mapping-form-row"]') - .first() - .exists() - ).toBe(true); - }); + .find('button[data-test-subj="case-configure-third-party-select-title"]') + .simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-mapping-description"]').simulate('click'); + wrapper.update(); - test('it shows the update button', () => { - expect( + expect(onChangeMapping).toHaveBeenCalledWith([ + { source: 'title', target: 'description', actionType: 'overwrite' }, + { source: 'description', target: 'not_mapped', actionType: 'append' }, + { source: 'comments', target: 'comments', actionType: 'append' }, + ]); + }); + + test('it updates actionType correctly', () => { wrapper - .find('[data-test-subj="case-mapping-update-connector-button"]') - .first() - .exists() - ).toBe(true); - }); + .find('button[data-test-subj="case-configure-action-type-select-title"]') + .simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="edit-update-option-nothing"]').simulate('click'); + wrapper.update(); + + expect(onChangeMapping).toHaveBeenCalledWith([ + { source: 'title', target: 'short_description', actionType: 'nothing' }, + { source: 'description', target: 'description', actionType: 'append' }, + { source: 'comments', target: 'comments', actionType: 'append' }, + ]); + }); - test('it shows the field mapping', () => { - expect( + test('it shows the correct action types', () => { wrapper - .find('[data-test-subj="case-mapping-field"]') - .first() - .exists() - ).toBe(true); + .find('button[data-test-subj="case-configure-action-type-select-title"]') + .simulate('click'); + wrapper.update(); + expect( + wrapper + .find('button[data-test-subj="edit-update-option-nothing"]') + .first() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('button[data-test-subj="edit-update-option-overwrite"]') + .first() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('button[data-test-subj="edit-update-option-append"]') + .first() + .exists() + ).toBeTruthy(); + }); + }); + + describe('Connectors', () => { + describe('ServiceNow', () => { + test('it shows the correct thirdParty fields for title', () => { + wrapper + .find('button[data-test-subj="case-configure-third-party-select-title"]') + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('button[data-test-subj="dropdown-mapping-short_description"]') + .first() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('button[data-test-subj="dropdown-mapping-description"]') + .first() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('button[data-test-subj="dropdown-mapping-not_mapped"]') + .first() + .exists() + ).toBeTruthy(); + }); + + test('it shows the correct thirdParty fields for description', () => { + wrapper + .find('button[data-test-subj="case-configure-third-party-select-description"]') + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('button[data-test-subj="dropdown-mapping-short_description"]') + .first() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('button[data-test-subj="dropdown-mapping-description"]') + .first() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('button[data-test-subj="dropdown-mapping-not_mapped"]') + .first() + .exists() + ).toBeTruthy(); + }); + + test('it shows the correct thirdParty fields for comments', () => { + wrapper + .find('button[data-test-subj="case-configure-third-party-select-comments"]') + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('button[data-test-subj="dropdown-mapping-comments"]') + .first() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('button[data-test-subj="dropdown-mapping-not_mapped"]') + .first() + .exists() + ).toBeTruthy(); + }); + }); + + describe('Jira', () => { + beforeEach(() => { + wrapper = mount(, { + wrappingComponent: TestProviders, + }); + }); + + test('it shows the correct thirdParty fields for title', () => { + wrapper + .find('button[data-test-subj="case-configure-third-party-select-title"]') + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('button[data-test-subj="dropdown-mapping-summary"]') + .first() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('button[data-test-subj="dropdown-mapping-description"]') + .first() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('button[data-test-subj="dropdown-mapping-not_mapped"]') + .first() + .exists() + ).toBeTruthy(); + }); + + test('it shows the correct thirdParty fields for description', () => { + wrapper + .find('button[data-test-subj="case-configure-third-party-select-description"]') + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('button[data-test-subj="dropdown-mapping-summary"]') + .first() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('button[data-test-subj="dropdown-mapping-description"]') + .first() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('button[data-test-subj="dropdown-mapping-not_mapped"]') + .first() + .exists() + ).toBeTruthy(); + }); + + test('it shows the correct thirdParty fields for comments', () => { + wrapper + .find('button[data-test-subj="case-configure-third-party-select-comments"]') + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('button[data-test-subj="dropdown-mapping-comments"]') + .first() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('button[data-test-subj="dropdown-mapping-not_mapped"]') + .first() + .exists() + ).toBeTruthy(); + }); + }); }); }); diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx index 7340a49f6d0bbc..acbcdac68a1340 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx @@ -24,6 +24,7 @@ export interface MappingProps { disabled: boolean; updateConnectorDisabled: boolean; mapping: CasesConfigurationMapping[] | null; + connectorActionTypeId: string; onChangeMapping: (newMapping: CasesConfigurationMapping[]) => void; setEditFlyoutVisibility: () => void; } @@ -39,6 +40,7 @@ const MappingComponent: React.FC = ({ mapping, onChangeMapping, setEditFlyoutVisibility, + connectorActionTypeId, }) => { return ( = ({