From d4a631cf8e2e6ef9fcc4c86256013d4389d5df7d Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Tue, 15 Dec 2020 08:31:59 -0500 Subject: [PATCH 01/10] [Security Solution][Resolver] Fixing resolver functional tests (#85647) * Fixing resolver functional tests * Import the animation constant * Only check specific nodes instead of all the ones in view * Removing check for link text * updating test description * Adding comments --- .../resolver/view/panels/event_detail.tsx | 10 +- .../view/panels/node_events_of_type.tsx | 1 + .../timeline/body/column_headers/index.tsx | 6 +- .../apps/endpoint/resolver.ts | 159 ++++------ .../page_objects/hosts_page.ts | 290 ++++++++++++------ 5 files changed, 261 insertions(+), 205 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx index 003182bd5f1b7..3c134eb6ba512 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx @@ -8,7 +8,7 @@ /* eslint-disable react/display-name */ -import React, { memo, useMemo, Fragment } from 'react'; +import React, { memo, useMemo, Fragment, HTMLAttributes } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSpacer, EuiText, EuiDescriptionList, EuiTextColor, EuiTitle } from '@elastic/eui'; @@ -246,7 +246,12 @@ function EventDetailBreadcrumbs({ panelParameters: { nodeID, eventCategory: breadcrumbEventCategory }, }); const breadcrumbs = useMemo(() => { - const crumbs = [ + const crumbs: Array< + { + text: JSX.Element | string; + 'data-test-subj'?: string; + } & HTMLAttributes + > = [ { text: i18n.translate( 'xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.events', @@ -254,6 +259,7 @@ function EventDetailBreadcrumbs({ defaultMessage: 'Events', } ), + 'data-test-subj': 'resolver:event-detail:breadcrumbs:node-list-link', ...nodesLinkNavProps, }, { diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx index fbfba38295ea4..2f6aa2ccbaa10 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx @@ -262,6 +262,7 @@ const NodeEventsInCategoryBreadcrumbs = memo(function ({ defaultMessage: 'Events', } ), + 'data-test-subj': 'resolver:node-events-in-category:breadcrumbs:node-list-link', ...nodesLinkNavProps, }, { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index e3808514856e7..ddeb1331564ec 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -304,7 +304,11 @@ export const ColumnHeadersComponent = ({ } className={fullScreen ? FULL_SCREEN_TOGGLED_CLASS_NAME : ''} color={fullScreen ? 'ghost' : 'primary'} - data-test-subj="full-screen" + data-test-subj={ + // a full screen button gets created for timeline and for the host page + // this sets the data-test-subj for each case so that tests can differentiate between them + timelineId === TimelineId.active ? 'full-screen-active' : 'full-screen' + } iconType="fullScreen" onClick={toggleFullScreen} /> diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts index 1d7b2861a1a31..debde49e35871 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts @@ -12,10 +12,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const esArchiver = getService('esArchiver'); const browser = getService('browser'); - const queryBar = getService('queryBar'); - // FLAKY: https://github.com/elastic/kibana/issues/85085 - describe.skip('Endpoint Event Resolver', function () { + describe('Endpoint Event Resolver', function () { before(async () => { await pageObjects.hosts.navigateToSecurityHostsPage(); await pageObjects.common.dismissBanner(); @@ -28,7 +26,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { await esArchiver.load('empty_kibana'); await esArchiver.load('endpoint/resolver_tree/functions', { useCreate: true }); - await pageObjects.hosts.navigateToEventsPanel(); await pageObjects.hosts.executeQueryAndOpenResolver('event.dataset : endpoint.events.file'); }); after(async () => { @@ -194,114 +191,74 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { } await (await testSubjects.find('resolver:graph-controls:zoom-in')).click(); }); - - it('Check Related Events for event.file Node', async () => { - const expectedData = [ - '17 authentication', - '1 registry', - '17 session', - '8 file', - '1 registry', - ]; - await pageObjects.hosts.runNodeEvents(expectedData); - }); }); - describe('Resolver Tree events', function () { - const expectedData = [ - '17 authentication', - '1 registry', - '17 session', - '80 registry', - '8 network', - '60 registry', - ]; + describe('node related event pills', function () { + /** + * Verifies that the pills of a node have the correct text. + * + * @param id the node ID to verify the pills for. + * @param expectedPills a map of expected pills for all nodes + */ + const verifyPills = async (id: string, expectedPills: Set) => { + const relatedEventPills = await pageObjects.hosts.findNodePills(id); + expect(relatedEventPills.length).to.equal(expectedPills.size); + for (const pill of relatedEventPills) { + const pillText = await pill._webElement.getText(); + // check that we have the pill text in our expected map + expect(expectedPills.has(pillText)).to.equal(true); + } + }; + before(async () => { await esArchiver.load('empty_kibana'); - await esArchiver.load('endpoint/resolver_tree/events', { useCreate: true }); - await queryBar.setQuery(''); - await queryBar.submitQuery(); + await esArchiver.load('endpoint/resolver_tree/alert_events', { useCreate: true }); }); after(async () => { await pageObjects.hosts.deleteDataStreams(); }); - it('Check Related Events for event.process Node', async () => { - await pageObjects.hosts.navigateToEventsPanel(); - await pageObjects.hosts.executeQueryAndOpenResolver( - 'event.dataset : endpoint.events.process' - ); - await pageObjects.hosts.runNodeEvents(expectedData); - }); + describe('endpoint.alerts filter', () => { + before(async () => { + await pageObjects.hosts.executeQueryAndOpenResolver('event.dataset : endpoint.alerts'); + await pageObjects.hosts.clickZoomOut(); + await browser.setWindowSize(2100, 1500); + }); - it('Check Related Events for event.security Node', async () => { - await pageObjects.hosts.navigateToEventsPanel(); - await pageObjects.hosts.executeQueryAndOpenResolver( - 'event.dataset : endpoint.events.security' - ); - await pageObjects.hosts.runNodeEvents(expectedData); - }); + it('has the correct pill text', async () => { + const expectedData: Map> = new Map([ + [ + 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTc2MzYtMTMyNDc2MTQ0NDIuOTU5MTE2NjAw', + new Set(['1 library']), + ], + [ + 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTMxMTYtMTMyNDcyNDk0MjQuOTg4ODI4NjAw', + new Set(['157 file', '520 registry']), + ], + [ + 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTUwODQtMTMyNDc2MTQ0NDIuOTcyODQ3MjAw', + new Set(), + ], + [ + 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTg2OTYtMTMyNDc2MTQ0MjEuNjc1MzY0OTAw', + new Set(['3 file']), + ], + [ + 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTcyNjAtMTMyNDc2MTQ0MjIuMjQwNDI2MTAw', + new Set(), + ], + [ + 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTczMDAtMTMyNDc2MTQ0MjEuNjg2NzI4NTAw', + new Set(), + ], + ]); - it('Check Related Events for event.registry Node', async () => { - await pageObjects.hosts.navigateToEventsPanel(); - await pageObjects.hosts.executeQueryAndOpenResolver( - 'event.dataset : endpoint.events.registry' - ); - await pageObjects.hosts.runNodeEvents(expectedData); - }); - - it('Check Related Events for event.network Node', async () => { - await pageObjects.hosts.navigateToEventsPanel(); - await pageObjects.hosts.executeQueryAndOpenResolver( - 'event.dataset : endpoint.events.network' - ); - await pageObjects.hosts.runNodeEvents(expectedData); - }); - - it('Check Related Events for event.library Node', async () => { - await esArchiver.load('empty_kibana'); - await esArchiver.load('endpoint/resolver_tree/library_events', { useCreate: true }); - await queryBar.setQuery(''); - await queryBar.submitQuery(); - const expectedLibraryData = [ - '1 authentication', - '1 session', - '329 network', - '1 library', - '1 library', - ]; - await pageObjects.hosts.navigateToEventsPanel(); - await pageObjects.hosts.executeQueryAndOpenResolver( - 'event.dataset : endpoint.events.library' - ); - // This lines will move the resolver view for clear visibility of the related events. - for (let i = 0; i < 7; i++) { - await (await testSubjects.find('resolver:graph-controls:west-button')).click(); - } - await pageObjects.hosts.runNodeEvents(expectedLibraryData); - }); - - it('Check Related Events for event.alert Node', async () => { - await esArchiver.load('empty_kibana'); - await esArchiver.load('endpoint/resolver_tree/alert_events', { useCreate: true }); - await queryBar.setQuery(''); - await queryBar.submitQuery(); - const expectedAlertData = [ - '1 library', - '157 file', - '520 registry', - '3 file', - '5 library', - '5 library', - ]; - await pageObjects.hosts.navigateToEventsPanel(); - await pageObjects.hosts.executeQueryAndOpenResolver('event.dataset : endpoint.alerts'); - await (await testSubjects.find('resolver:graph-controls:zoom-out')).click(); - await browser.setWindowSize(2100, 1500); - for (let i = 0; i < 2; i++) { - await (await testSubjects.find('resolver:graph-controls:east-button')).click(); - } - await pageObjects.hosts.runNodeEvents(expectedAlertData); + for (const [id, expectedPills] of expectedData.entries()) { + // center the node in the view + await pageObjects.hosts.clickNodeLinkInPanel(id); + await verifyPills(id, expectedPills); + } + }); }); }); }); diff --git a/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts b/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts index c76a5a7c22f60..09160a6ada15a 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts @@ -4,13 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; +import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper'; +import { nudgeAnimationDuration } from '../../../plugins/security_solution/public/resolver/store/camera/scaling_constants'; import { FtrProviderContext } from '../ftr_provider_context'; -import { deleteEventsStream } from '../../security_solution_endpoint_api_int/apis/data_stream_helper'; -import { deleteAlertsStream } from '../../security_solution_endpoint_api_int/apis/data_stream_helper'; -import { deleteMetadataStream } from '../../security_solution_endpoint_api_int/apis/data_stream_helper'; -import { deletePolicyStream } from '../../security_solution_endpoint_api_int/apis/data_stream_helper'; -import { deleteTelemetryStream } from '../../security_solution_endpoint_api_int/apis/data_stream_helper'; +import { + deleteEventsStream, + deleteAlertsStream, + deleteMetadataStream, + deletePolicyStream, + deleteTelemetryStream, +} from '../../security_solution_endpoint_api_int/apis/data_stream_helper'; + export interface DataStyle { left: string; top: string; @@ -22,6 +26,109 @@ export function SecurityHostsPageProvider({ getService, getPageObjects }: FtrPro const pageObjects = getPageObjects(['common', 'header']); const testSubjects = getService('testSubjects'); const queryBar = getService('queryBar'); + const find = getService('find'); + + /** + * Returns the node IDs for the visible nodes in the resolver graph. + */ + const findVisibleNodeIDs = async (): Promise => { + const visibleNodes = await testSubjects.findAll('resolver:node'); + return Promise.all( + visibleNodes.map(async (node: WebElementWrapper) => { + return node.getAttribute('data-test-resolver-node-id'); + }) + ); + }; + + /** + * This assumes you are on the process list in the panel and will find and click the node + * with the given ID to bring it into view in the graph. + * + * @param id the ID of the node to find and click. + */ + const clickNodeLinkInPanel = async (id: string): Promise => { + await navigateToProcessListInPanel(); + const panelNodeButton = await find.byCssSelector( + `[data-test-subj='resolver:node-list:node-link'][data-test-node-id='${id}']` + ); + + await panelNodeButton?.click(); + // ensure that we wait longer than the animation time + await pageObjects.common.sleep(nudgeAnimationDuration * 2); + }; + + /** + * Finds all the pills for a particular node. + * + * @param id the ID of the node + */ + const findNodePills = async (id: string): Promise => { + return testSubjects.findAllDescendant( + 'resolver:map:node-submenu-item', + await find.byCssSelector( + `[data-test-subj='resolver:node'][data-test-resolver-node-id='${id}']` + ) + ); + }; + + /** + * Navigate back to the process list view in the panel. + */ + const navigateToProcessListInPanel = async () => { + const [ + isOnNodeListPage, + isOnCategoryPage, + isOnNodeDetailsPage, + isOnRelatedEventDetailsPage, + ] = await Promise.all([ + testSubjects.exists('resolver:node-list', { timeout: 1 }), + testSubjects.exists('resolver:node-events-in-category:breadcrumbs:node-list-link', { + timeout: 1, + }), + testSubjects.exists('resolver:node-detail:breadcrumbs:node-list-link', { timeout: 1 }), + testSubjects.exists('resolver:event-detail:breadcrumbs:node-list-link', { timeout: 1 }), + ]); + + if (isOnNodeListPage) { + return; + } else if (isOnCategoryPage) { + await ( + await testSubjects.find('resolver:node-events-in-category:breadcrumbs:node-list-link') + ).click(); + } else if (isOnNodeDetailsPage) { + await (await testSubjects.find('resolver:node-detail:breadcrumbs:node-list-link')).click(); + } else if (isOnRelatedEventDetailsPage) { + await (await testSubjects.find('resolver:event-detail:breadcrumbs:node-list-link')).click(); + } else { + // unknown page + return; + } + + await pageObjects.common.sleep(100); + }; + + /** + * Click the zoom out control. + */ + const clickZoomOut = async () => { + await (await testSubjects.find('resolver:graph-controls:zoom-out')).click(); + }; + + /** + * Navigate to Events Panel + */ + const navigateToEventsPanel = async () => { + const isFullScreen = await testSubjects.exists('exit-full-screen', { timeout: 400 }); + if (isFullScreen) { + await (await testSubjects.find('exit-full-screen')).click(); + } + + if (!(await testSubjects.exists('investigate-in-resolver-button', { timeout: 400 }))) { + await (await testSubjects.find('navigation-hosts')).click(); + await testSubjects.click('navigation-events'); + await testSubjects.existOrFail('event'); + } + }; /** * @function parseStyles @@ -54,101 +161,82 @@ export function SecurityHostsPageProvider({ getService, getPageObjects }: FtrPro }), {} ); + + /** + * Navigate to the Security Hosts page + */ + const navigateToSecurityHostsPage = async () => { + await pageObjects.common.navigateToUrlWithBrowserHistory('security', '/hosts/AllHosts'); + await pageObjects.header.waitUntilLoadingHasFinished(); + }; + + /** + * Finds a table and returns the data in a nested array with row 0 is the headers if they exist. + * It uses euiTableCellContent to avoid polluting the array data with the euiTableRowCell__mobileHeader data. + * @param dataTestSubj + * @param element + * @returns Promise + */ + const getEndpointEventResolverNodeData = async (dataTestSubj: string, element: string) => { + await testSubjects.exists(dataTestSubj); + const Elements = await testSubjects.findAll(dataTestSubj); + const $ = []; + for (const value of Elements) { + $.push(await value.getAttribute(element)); + } + return $; + }; + + /** + * Gets a array of not parsed styles and returns the Array of parsed styles. + * @returns Promise + */ + const parseStyles = async () => { + const tableData = await getEndpointEventResolverNodeData('resolver:node', 'style'); + const styles: DataStyle[] = []; + for (let i = 1; i < tableData.length; i++) { + const eachStyle = parseStyle(tableData[i]); + styles.push({ + top: eachStyle.top ?? '', + height: eachStyle.height ?? '', + left: eachStyle.left ?? '', + width: eachStyle.width ?? '', + }); + } + return styles; + }; + /** + * Deletes DataStreams from Index Management. + */ + const deleteDataStreams = async () => { + await deleteEventsStream(getService); + await deleteAlertsStream(getService); + await deletePolicyStream(getService); + await deleteMetadataStream(getService); + await deleteTelemetryStream(getService); + }; + + /** + * execute Query And Open Resolver + */ + const executeQueryAndOpenResolver = async (query: string) => { + await navigateToEventsPanel(); + await queryBar.setQuery(query); + await queryBar.submitQuery(); + await testSubjects.click('full-screen'); + await testSubjects.click('investigate-in-resolver-button'); + }; + return { - /** - * Navigate to the Security Hosts page - */ - async navigateToSecurityHostsPage() { - await pageObjects.common.navigateToUrlWithBrowserHistory('security', '/hosts/AllHosts'); - await pageObjects.header.waitUntilLoadingHasFinished(); - }, - /** - * Finds a table and returns the data in a nested array with row 0 is the headers if they exist. - * It uses euiTableCellContent to avoid poluting the array data with the euiTableRowCell__mobileHeader data. - * @param dataTestSubj - * @param element - * @returns Promise - */ - async getEndpointEventResolverNodeData(dataTestSubj: string, element: string) { - await testSubjects.exists(dataTestSubj); - const Elements = await testSubjects.findAll(dataTestSubj); - const $ = []; - for (const value of Elements) { - $.push(await value.getAttribute(element)); - } - return $; - }, - - /** - * Gets a array of not parsed styles and returns the Array of parsed styles. - * @returns Promise - */ - async parseStyles() { - const tableData = await this.getEndpointEventResolverNodeData('resolver:node', 'style'); - const styles: DataStyle[] = []; - for (let i = 1; i < tableData.length; i++) { - const eachStyle = parseStyle(tableData[i]); - styles.push({ - top: eachStyle.top ?? '', - height: eachStyle.height ?? '', - left: eachStyle.left ?? '', - width: eachStyle.width ?? '', - }); - } - return styles; - }, - /** - * Deletes DataStreams from Index Management. - */ - async deleteDataStreams() { - await deleteEventsStream(getService); - await deleteAlertsStream(getService); - await deletePolicyStream(getService); - await deleteMetadataStream(getService); - await deleteTelemetryStream(getService); - }, - /** - * Runs Nodes Events - */ - async runNodeEvents(expectedData: string[]) { - await testSubjects.exists('resolver:submenu:button', { timeout: 400 }); - const NodeSubmenuButtons = await testSubjects.findAll('resolver:submenu:button'); - for (let b = 0; b < NodeSubmenuButtons.length; b++) { - await (await testSubjects.findAll('resolver:submenu:button'))[b].click(); - } - await testSubjects.exists('resolver:map:node-submenu-item', { timeout: 400 }); - const NodeSubmenuItems = await testSubjects.findAll('resolver:map:node-submenu-item'); - for (let i = 0; i < NodeSubmenuItems.length; i++) { - await (await testSubjects.findAll('resolver:map:node-submenu-item'))[i].click(); - const Events = await testSubjects.findAll('resolver:map:node-submenu-item'); - // this sleep is for the AMP enabled run - await pageObjects.common.sleep(300); - const EventName = await Events[i]._webElement.getText(); - const LinkText = await testSubjects.find('resolver:breadcrumbs:last'); - const linkText = await LinkText._webElement.getText(); - expect(EventName).to.equal(linkText); - expect(EventName).to.equal(expectedData[i]); - } - await testSubjects.click('full-screen'); - }, - /** - * Navigate to Events Panel - */ - async navigateToEventsPanel() { - if (!(await testSubjects.exists('investigate-in-resolver-button', { timeout: 400 }))) { - await (await testSubjects.find('navigation-hosts')).click(); - await testSubjects.click('navigation-events'); - await testSubjects.existOrFail('event'); - } - }, - /** - * execute Query And Open Resolver - */ - async executeQueryAndOpenResolver(query: string) { - await queryBar.setQuery(query); - await queryBar.submitQuery(); - await testSubjects.click('full-screen'); - await testSubjects.click('investigate-in-resolver-button'); - }, + navigateToProcessListInPanel, + findNodePills, + clickNodeLinkInPanel, + findVisibleNodeIDs, + clickZoomOut, + navigateToEventsPanel, + navigateToSecurityHostsPage, + parseStyles, + deleteDataStreams, + executeQueryAndOpenResolver, }; } From 335cd1f6fc15f14c6f388fe7055e804fdd284823 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 15 Dec 2020 07:06:11 -0700 Subject: [PATCH 02/10] [Security Solution] [Cases] Move field mappings from actions to cases (#84587) --- .../run_find_plugins_with_circular_deps.ts | 1 + x-pack/plugins/actions/README.md | 3 - .../builtin_action_types/case/constants.ts | 7 - .../builtin_action_types/case/schema.ts | 43 -- .../case/transformers.test.ts | 131 ----- .../builtin_action_types/case/transformers.ts | 29 - .../builtin_action_types/case/translations.ts | 55 -- .../server/builtin_action_types/case/types.ts | 54 -- .../builtin_action_types/case/utils.test.ts | 494 ---------------- .../server/builtin_action_types/case/utils.ts | 111 ---- .../builtin_action_types/jira/api.test.ts | 518 +---------------- .../server/builtin_action_types/jira/api.ts | 86 +-- .../server/builtin_action_types/jira/index.ts | 26 +- .../server/builtin_action_types/jira/mocks.ts | 55 +- .../builtin_action_types/jira/schema.ts | 32 +- .../builtin_action_types/jira/service.test.ts | 12 - .../builtin_action_types/jira/service.ts | 12 +- .../builtin_action_types/jira/translations.ts | 8 - .../server/builtin_action_types/jira/types.ts | 56 +- .../builtin_action_types/jira/validators.ts | 8 - .../resilient/api.test.ts | 476 +-------------- .../builtin_action_types/resilient/api.ts | 80 +-- .../builtin_action_types/resilient/index.ts | 15 +- .../builtin_action_types/resilient/mocks.ts | 52 +- .../builtin_action_types/resilient/schema.ts | 28 +- .../resilient/service.test.ts | 12 - .../resilient/translations.ts | 8 - .../builtin_action_types/resilient/types.ts | 37 +- .../resilient/validators.ts | 8 - .../servicenow/api.test.ts | 449 +-------------- .../builtin_action_types/servicenow/api.ts | 93 +-- .../builtin_action_types/servicenow/index.ts | 13 +- .../builtin_action_types/servicenow/mocks.ts | 54 +- .../builtin_action_types/servicenow/schema.ts | 31 +- .../servicenow/service.test.ts | 4 +- .../servicenow/service.ts | 26 +- .../servicenow/translations.ts | 8 - .../builtin_action_types/servicenow/types.ts | 28 +- .../servicenow/validators.ts | 8 - .../server/saved_objects/migrations.test.ts | 15 + .../server/saved_objects/migrations.ts | 35 +- x-pack/plugins/actions/server/types.ts | 3 + x-pack/plugins/case/common/api/cases/case.ts | 68 +-- .../case/common/api/cases/configure.ts | 56 +- .../case/common/api/connectors/index.ts | 8 +- .../case/common/api/connectors/jira.ts | 6 - .../case/common/api/connectors/mappings.ts | 191 ++++++ .../case/common/api/connectors/resilient.ts | 6 - .../case/common/api/connectors/servicenow.ts | 6 - x-pack/plugins/case/common/api/helpers.ts | 4 + .../plugins/case/common/api/saved_object.ts | 1 + x-pack/plugins/case/common/constants.ts | 2 + .../server/client/configure/get_fields.ts | 31 + .../server/client/configure/get_mappings.ts | 58 ++ .../server/client/configure/utils.test.ts | 545 ++++++++++++++++++ .../case/server/client/configure/utils.ts | 169 ++++++ .../plugins/case/server/client/index.test.ts | 53 +- x-pack/plugins/case/server/client/index.ts | 50 +- x-pack/plugins/case/server/client/mocks.ts | 14 +- x-pack/plugins/case/server/client/types.ts | 30 +- .../case/server/connectors/case/index.test.ts | 3 + .../case/server/connectors/case/index.ts | 16 +- .../plugins/case/server/connectors/index.ts | 4 + x-pack/plugins/case/server/index.ts | 2 +- x-pack/plugins/case/server/plugin.ts | 21 +- .../__fixtures__/create_mock_so_repository.ts | 11 + .../routes/api/__fixtures__/mock_router.ts | 5 +- .../api/__fixtures__/mock_saved_objects.ts | 31 +- .../routes/api/__fixtures__/route_contexts.ts | 10 +- .../routes/api/__mocks__/request_responses.ts | 39 -- .../api/cases/configure/get_configure.test.ts | 8 +- .../api/cases/configure/get_configure.ts | 21 +- .../cases/configure/get_connectors.test.ts | 71 +-- .../api/cases/configure/get_connectors.ts | 35 +- .../routes/api/cases/configure/get_fields.ts | 70 +++ .../server/routes/api/cases/configure/mock.ts | 53 ++ .../cases/configure/patch_configure.test.ts | 6 + .../api/cases/configure/patch_configure.ts | 21 +- .../cases/configure/post_configure.test.ts | 17 +- .../api/cases/configure/post_configure.ts | 16 +- .../cases/configure/post_push_to_service.ts | 80 +++ .../routes/api/cases/configure/utils.test.ts | 385 +++++++++++++ .../routes/api/cases/configure/utils.ts | 237 ++++++++ .../plugins/case/server/routes/api/index.ts | 4 + .../plugins/case/server/routes/api/types.ts | 4 +- .../saved_object_types/connector_mappings.ts | 32 + .../case/server/saved_object_types/index.ts | 4 + .../services/connector_mappings/index.ts | 59 ++ x-pack/plugins/case/server/services/index.ts | 1 + x-pack/plugins/case/server/services/mocks.ts | 9 +- .../integration/cases_connectors.spec.ts | 29 +- .../security_solution/cypress/objects/case.ts | 24 - .../cypress/screens/configure_cases.ts | 2 + .../configure_cases/__mock__/index.tsx | 7 +- .../configure_cases/connectors.test.tsx | 34 +- .../components/configure_cases/connectors.tsx | 57 +- .../configure_cases/field_mapping.test.tsx | 65 +-- .../configure_cases/field_mapping.tsx | 153 ++--- .../field_mapping_row.test.tsx | 114 ---- .../configure_cases/field_mapping_row.tsx | 80 --- .../field_mapping_row_static.tsx | 59 ++ .../components/configure_cases/index.test.tsx | 12 +- .../components/configure_cases/index.tsx | 9 +- .../configure_cases/mapping.test.tsx | 195 +------ .../components/configure_cases/mapping.tsx | 81 +-- .../configure_cases/translations.ts | 84 ++- .../components/configure_cases/utils.test.tsx | 30 +- .../cases/components/configure_cases/utils.ts | 10 +- .../cases/components/connectors/index.ts | 1 - .../cases/components/connectors/utils.ts | 21 - .../cases/components/settings/jira/api.ts | 2 +- .../components/settings/resilient/api.ts | 2 +- .../public/cases/containers/api.test.tsx | 21 +- .../public/cases/containers/api.ts | 56 +- .../public/cases/containers/configure/api.ts | 9 + .../public/cases/containers/configure/mock.ts | 50 +- .../cases/containers/configure/types.ts | 32 +- .../configure/use_configure.test.tsx | 38 +- .../containers/configure/use_configure.tsx | 39 +- .../public/cases/containers/mock.ts | 2 +- .../public/cases/containers/translations.ts | 7 + .../public/cases/containers/types.ts | 5 + .../cases/containers/use_get_fields.tsx | 82 +++ .../use_post_push_to_service.test.tsx | 2 + .../containers/use_post_push_to_service.tsx | 1 + .../translations/translations/ja-JP.json | 46 -- .../translations/translations/zh-CN.json | 46 -- .../case_mappings/field_mapping.tsx | 140 ----- .../case_mappings/field_mapping_row.tsx | 78 --- .../case_mappings/index.ts | 10 - .../case_mappings/translations.ts | 190 ------ .../case_mappings/types.ts | 21 - .../case_mappings/utils.ts | 48 -- .../builtin_action_types/jira/jira.test.tsx | 8 +- .../builtin_action_types/jira/jira.tsx | 10 +- .../jira/jira_connectors.test.tsx | 1 - .../jira/jira_connectors.tsx | 84 +-- .../jira/jira_params.test.tsx | 51 +- .../builtin_action_types/jira/jira_params.tsx | 260 ++++----- .../builtin_action_types/jira/types.ts | 23 +- .../resilient/resilient.test.tsx | 8 +- .../resilient/resilient.tsx | 11 +- .../resilient/resilient_connectors.test.tsx | 1 - .../resilient/resilient_connectors.tsx | 53 +- .../resilient/resilient_params.test.tsx | 44 +- .../resilient/resilient_params.tsx | 287 ++++----- .../builtin_action_types/resilient/types.ts | 19 +- .../servicenow/servicenow.test.tsx | 8 +- .../servicenow/servicenow.tsx | 10 +- .../servicenow/servicenow_connectors.test.tsx | 1 - .../servicenow/servicenow_connectors.tsx | 52 +- .../servicenow/servicenow_params.test.tsx | 56 +- .../servicenow/servicenow_params.tsx | 243 ++++---- .../servicenow/translations.ts | 2 +- .../builtin_action_types/servicenow/types.ts | 20 +- .../action_form.test.tsx | 4 +- .../connector_edit_flyout.tsx | 17 +- .../sections/alert_form/alert_reducer.ts | 6 +- .../actions/builtin_action_types/jira.ts | 39 +- .../actions/builtin_action_types/resilient.ts | 33 +- .../builtin_action_types/servicenow.ts | 38 +- .../actions_simulators/server/plugin.ts | 3 + .../server/servicenow_simulation.ts | 38 ++ .../actions/builtin_action_types/jira.ts | 134 +---- .../actions/builtin_action_types/resilient.ts | 130 +---- .../builtin_action_types/servicenow.ts | 153 +---- .../spaces_only/tests/actions/migrations.ts | 24 +- .../basic/tests/cases/push_case.ts | 32 +- .../user_actions/get_all_user_actions.ts | 16 +- .../basic/tests/configure/get_connectors.ts | 108 ---- .../case_api_integration/common/config.ts | 45 ++ .../case_api_integration/common/lib/mock.ts | 2 +- .../case_api_integration/common/lib/utils.ts | 122 +--- 173 files changed, 3796 insertions(+), 6191 deletions(-) delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/case/constants.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/case/schema.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/case/transformers.test.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/case/transformers.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/case/translations.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/case/types.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/case/utils.ts create mode 100644 x-pack/plugins/case/common/api/connectors/mappings.ts create mode 100644 x-pack/plugins/case/server/client/configure/get_fields.ts create mode 100644 x-pack/plugins/case/server/client/configure/get_mappings.ts create mode 100644 x-pack/plugins/case/server/client/configure/utils.test.ts create mode 100644 x-pack/plugins/case/server/client/configure/utils.ts create mode 100644 x-pack/plugins/case/server/routes/api/cases/configure/get_fields.ts create mode 100644 x-pack/plugins/case/server/routes/api/cases/configure/mock.ts create mode 100644 x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.ts create mode 100644 x-pack/plugins/case/server/routes/api/cases/configure/utils.test.ts create mode 100644 x-pack/plugins/case/server/routes/api/cases/configure/utils.ts create mode 100644 x-pack/plugins/case/server/saved_object_types/connector_mappings.ts create mode 100644 x-pack/plugins/case/server/services/connector_mappings/index.ts delete mode 100644 x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row_static.tsx delete mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/utils.ts create mode 100644 x-pack/plugins/security_solution/public/cases/containers/use_get_fields.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping_row.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/index.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/translations.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/types.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/utils.ts diff --git a/src/dev/run_find_plugins_with_circular_deps.ts b/src/dev/run_find_plugins_with_circular_deps.ts index 65a03a87525d7..f4662820a1fb0 100644 --- a/src/dev/run_find_plugins_with_circular_deps.ts +++ b/src/dev/run_find_plugins_with_circular_deps.ts @@ -42,6 +42,7 @@ const allowedList: CircularDepList = new Set([ 'src/plugins/vis_default_editor -> src/plugins/visualize', 'src/plugins/visualizations -> src/plugins/visualize', 'x-pack/plugins/actions -> x-pack/plugins/case', + 'x-pack/plugins/case -> x-pack/plugins/security_solution', 'x-pack/plugins/apm -> x-pack/plugins/infra', 'x-pack/plugins/lists -> x-pack/plugins/security_solution', 'x-pack/plugins/security -> x-pack/plugins/spaces', diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index fb0293dca5ff4..12c3ab12a6998 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -553,7 +553,6 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a | Property | Description | Type | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------- | | apiUrl | ServiceNow instance URL. | string | -| incidentConfiguration | Optional property and specific to **Cases only**. If defined, the object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the ServiceNow field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'short_description', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in ServiceNow and will be overwrite on each update. | object _(optional)_ | ### `secrets` @@ -600,7 +599,6 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla | Property | Description | Type | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | | apiUrl | Jira instance URL. | string | -| incidentConfiguration | Optional property and specific to **Cases only**. if defined, the object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in Jira and will be overwrite on each update. | object _(optional)_ | ### `secrets` @@ -653,7 +651,6 @@ ID: `.resilient` | Property | Description | Type | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | | apiUrl | IBM Resilient instance URL. | string | -| incidentConfiguration | Optional property and specific to **Cases only**. If defined, the object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in IBM Resilient and will be overwrite on each update. | object | ### `secrets` diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/constants.ts b/x-pack/plugins/actions/server/builtin_action_types/case/constants.ts deleted file mode 100644 index 1f2bc7f5e8e53..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/case/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const SUPPORTED_SOURCE_FIELDS = ['title', 'comments', 'description']; diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts deleted file mode 100644 index 5a23eb89339e6..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { schema } from '@kbn/config-schema'; - -export const MappingActionType = schema.oneOf([ - schema.literal('nothing'), - schema.literal('overwrite'), - schema.literal('append'), -]); - -export const MapRecordSchema = schema.object({ - source: schema.string(), - target: schema.string(), - actionType: MappingActionType, -}); - -export const IncidentConfigurationSchema = schema.object({ - mapping: schema.arrayOf(MapRecordSchema), -}); - -export const UserSchema = schema.object({ - fullName: schema.nullable(schema.string()), - username: schema.nullable(schema.string()), -}); - -export const EntityInformation = { - createdAt: schema.nullable(schema.string()), - createdBy: schema.nullable(UserSchema), - updatedAt: schema.nullable(schema.string()), - updatedBy: schema.nullable(UserSchema), -}; - -export const EntityInformationSchema = schema.object(EntityInformation); - -export const CommentSchema = schema.object({ - commentId: schema.string(), - comment: schema.string(), - ...EntityInformation, -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/transformers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/case/transformers.test.ts deleted file mode 100644 index 75dcab65ee9f2..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/case/transformers.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { transformers } from './transformers'; - -const { informationCreated, informationUpdated, informationAdded, append } = transformers; - -describe('informationCreated', () => { - test('transforms correctly', () => { - const res = informationCreated({ - value: 'a value', - date: '2020-04-15T08:19:27.400Z', - user: 'elastic', - }); - expect(res).toEqual({ value: 'a value (created at 2020-04-15T08:19:27.400Z by elastic)' }); - }); - - test('transforms correctly without optional fields', () => { - const res = informationCreated({ - value: 'a value', - }); - expect(res).toEqual({ value: 'a value (created at by )' }); - }); - - test('returns correctly rest fields', () => { - const res = informationCreated({ - value: 'a value', - date: '2020-04-15T08:19:27.400Z', - user: 'elastic', - previousValue: 'previous value', - }); - expect(res).toEqual({ - value: 'a value (created at 2020-04-15T08:19:27.400Z by elastic)', - previousValue: 'previous value', - }); - }); -}); - -describe('informationUpdated', () => { - test('transforms correctly', () => { - const res = informationUpdated({ - value: 'a value', - date: '2020-04-15T08:19:27.400Z', - user: 'elastic', - }); - expect(res).toEqual({ value: 'a value (updated at 2020-04-15T08:19:27.400Z by elastic)' }); - }); - - test('transforms correctly without optional fields', () => { - const res = informationUpdated({ - value: 'a value', - }); - expect(res).toEqual({ value: 'a value (updated at by )' }); - }); - - test('returns correctly rest fields', () => { - const res = informationUpdated({ - value: 'a value', - date: '2020-04-15T08:19:27.400Z', - user: 'elastic', - previousValue: 'previous value', - }); - expect(res).toEqual({ - value: 'a value (updated at 2020-04-15T08:19:27.400Z by elastic)', - previousValue: 'previous value', - }); - }); -}); - -describe('informationAdded', () => { - test('transforms correctly', () => { - const res = informationAdded({ - value: 'a value', - date: '2020-04-15T08:19:27.400Z', - user: 'elastic', - }); - expect(res).toEqual({ value: 'a value (added at 2020-04-15T08:19:27.400Z by elastic)' }); - }); - - test('transforms correctly without optional fields', () => { - const res = informationAdded({ - value: 'a value', - }); - expect(res).toEqual({ value: 'a value (added at by )' }); - }); - - test('returns correctly rest fields', () => { - const res = informationAdded({ - value: 'a value', - date: '2020-04-15T08:19:27.400Z', - user: 'elastic', - previousValue: 'previous value', - }); - expect(res).toEqual({ - value: 'a value (added at 2020-04-15T08:19:27.400Z by elastic)', - previousValue: 'previous value', - }); - }); -}); - -describe('append', () => { - test('transforms correctly', () => { - const res = append({ - value: 'a value', - previousValue: 'previous value', - }); - expect(res).toEqual({ value: 'previous value \r\na value' }); - }); - - test('transforms correctly without optional fields', () => { - const res = append({ - value: 'a value', - }); - expect(res).toEqual({ value: 'a value' }); - }); - - test('returns correctly rest fields', () => { - const res = append({ - value: 'a value', - user: 'elastic', - previousValue: 'previous value', - }); - expect(res).toEqual({ - value: 'previous value \r\na value', - user: 'elastic', - }); - }); -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/transformers.ts b/x-pack/plugins/actions/server/builtin_action_types/case/transformers.ts deleted file mode 100644 index 3dca1fd703430..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/case/transformers.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { TransformerArgs } from './types'; -import * as i18n from './translations'; - -export type Transformer = (args: TransformerArgs) => TransformerArgs; - -export const transformers: Record = { - informationCreated: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({ - value: `${value} ${i18n.FIELD_INFORMATION('create', date, user)}`, - ...rest, - }), - informationUpdated: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({ - value: `${value} ${i18n.FIELD_INFORMATION('update', date, user)}`, - ...rest, - }), - informationAdded: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({ - value: `${value} ${i18n.FIELD_INFORMATION('add', date, user)}`, - ...rest, - }), - append: ({ value, previousValue, ...rest }: TransformerArgs): TransformerArgs => ({ - value: previousValue ? `${previousValue} \r\n${value}` : `${value}`, - ...rest, - }), -}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/case/translations.ts deleted file mode 100644 index 4842728b0e4e7..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/case/translations.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const API_URL_REQUIRED = i18n.translate('xpack.actions.builtin.case.connectorApiNullError', { - defaultMessage: 'connector [apiUrl] is required', -}); - -export const FIELD_INFORMATION = ( - mode: string, - date: string | undefined, - user: string | undefined -) => { - switch (mode) { - case 'create': - return i18n.translate('xpack.actions.builtin.case.common.externalIncidentCreated', { - values: { date, user }, - defaultMessage: '(created at {date} by {user})', - }); - case 'update': - return i18n.translate('xpack.actions.builtin.case.common.externalIncidentUpdated', { - values: { date, user }, - defaultMessage: '(updated at {date} by {user})', - }); - case 'add': - return i18n.translate('xpack.actions.builtin.case.common.externalIncidentAdded', { - values: { date, user }, - defaultMessage: '(added at {date} by {user})', - }); - default: - return i18n.translate('xpack.actions.builtin.case.common.externalIncidentDefault', { - values: { date, user }, - defaultMessage: '(created at {date} by {user})', - }); - } -}; - -export const MAPPING_EMPTY = i18n.translate( - 'xpack.actions.builtin.case.configuration.emptyMapping', - { - defaultMessage: '[casesConfiguration.mapping]: expected non-empty but got empty', - } -); - -export const WHITE_LISTED_ERROR = (message: string) => - i18n.translate('xpack.actions.builtin.case.configuration.apiWhitelistError', { - defaultMessage: 'error configuring connector action: {message}', - values: { - message, - }, - }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts deleted file mode 100644 index 73d8297c638df..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { TypeOf } from '@kbn/config-schema'; -import { - IncidentConfigurationSchema, - MapRecordSchema, - CommentSchema, - EntityInformationSchema, -} from './schema'; - -export type IncidentConfiguration = TypeOf; -export type MapRecord = TypeOf; -export type Comment = TypeOf; -export type EntityInformation = TypeOf; - -export interface ExternalServiceCommentResponse { - commentId: string; - pushedDate: string; - externalCommentId?: string; -} - -export interface PipedField { - key: string; - value: string; - actionType: string; - pipes: string[]; -} - -export interface TransformFieldsArgs { - params: P; - fields: PipedField[]; - currentIncident?: S; -} - -export interface TransformerArgs { - value: string; - date?: string; - user?: string; - previousValue?: string; -} - -export interface AnyParams { - [index: string]: string | number | object | undefined | null; -} - -export interface PrepareFieldsForTransformArgs { - externalCase: Record; - mapping: Map; - defaultPipes?: string[]; -} diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts deleted file mode 100644 index 600e18eb5daff..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts +++ /dev/null @@ -1,494 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { - normalizeMapping, - buildMap, - mapParams, - prepareFieldsForTransformation, - transformFields, - transformComments, -} from './utils'; - -import { SUPPORTED_SOURCE_FIELDS } from './constants'; -import { Comment, MapRecord } from './types'; - -interface Entity { - createdAt: string | null; - createdBy: { fullName: string; username: string } | null; - updatedAt: string | null; - updatedBy: { fullName: string; username: string } | null; -} - -interface PushToServiceApiParams extends Entity { - savedObjectId: string; - title: string; - description: string | null; - externalId: string | null; - externalObject: Record; - comments: Comment[]; -} - -const mapping: MapRecord[] = [ - { source: 'title', target: 'short_description', actionType: 'overwrite' }, - { source: 'description', target: 'description', actionType: 'append' }, - { source: 'comments', target: 'comments', actionType: 'append' }, -]; - -const finalMapping: Map = new Map(); - -finalMapping.set('title', { - target: 'short_description', - actionType: 'overwrite', -}); - -finalMapping.set('description', { - target: 'description', - actionType: 'append', -}); - -finalMapping.set('comments', { - target: 'comments', - actionType: 'append', -}); - -finalMapping.set('short_description', { - target: 'title', - actionType: 'overwrite', -}); - -const maliciousMapping: MapRecord[] = [ - { source: '__proto__', target: 'short_description', actionType: 'nothing' }, - { source: 'description', target: '__proto__', actionType: 'nothing' }, - { source: 'comments', target: 'comments', actionType: 'nothing' }, - { source: 'unsupportedSource', target: 'comments', actionType: 'nothing' }, -]; - -const fullParams: PushToServiceApiParams = { - savedObjectId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', - title: 'a title', - description: 'a description', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - externalId: null, - externalObject: { - short_description: 'a title', - description: 'a description', - }, - comments: [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - comment: 'second comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ], -}; - -describe('normalizeMapping', () => { - test('remove malicious fields', () => { - const sanitizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); - expect( - sanitizedMapping.every((m) => m.source !== '__proto__' && m.target !== '__proto__') - ).toBe(true); - }); - - test('remove unsuppported source fields', () => { - const normalizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); - expect(normalizedMapping).not.toEqual( - expect.arrayContaining([ - expect.objectContaining({ - source: 'unsupportedSource', - target: 'comments', - actionType: 'nothing', - }), - ]) - ); - }); -}); - -describe('buildMap', () => { - test('builds sanitized Map', () => { - const finalMap = buildMap(maliciousMapping); - expect(finalMap.get('__proto__')).not.toBeDefined(); - }); - - test('builds Map correct', () => { - const final = buildMap(mapping); - expect(final).toEqual(finalMapping); - }); -}); - -describe('mapParams', () => { - test('maps params correctly', () => { - const params = { - savedObjectId: '123', - incidentId: '456', - title: 'Incident title', - description: 'Incident description', - }; - - const fields = mapParams(params, finalMapping); - - expect(fields).toEqual({ - short_description: 'Incident title', - description: 'Incident description', - }); - }); - - test('do not add fields not in mapping', () => { - const params = { - savedObjectId: '123', - incidentId: '456', - title: 'Incident title', - description: 'Incident description', - }; - const fields = mapParams(params, finalMapping); - - const { title, description, ...unexpectedFields } = params; - - expect(fields).not.toEqual(expect.objectContaining(unexpectedFields)); - }); -}); - -describe('prepareFieldsForTransformation', () => { - test('prepare fields with defaults', () => { - const res = prepareFieldsForTransformation({ - externalCase: fullParams.externalObject, - mapping: finalMapping, - }); - expect(res).toEqual([ - { - key: 'short_description', - value: 'a title', - actionType: 'overwrite', - pipes: ['informationCreated'], - }, - { - key: 'description', - value: 'a description', - actionType: 'append', - pipes: ['informationCreated', 'append'], - }, - ]); - }); - - test('prepare fields with default pipes', () => { - const res = prepareFieldsForTransformation({ - externalCase: fullParams.externalObject, - mapping: finalMapping, - defaultPipes: ['myTestPipe'], - }); - expect(res).toEqual([ - { - key: 'short_description', - value: 'a title', - actionType: 'overwrite', - pipes: ['myTestPipe'], - }, - { - key: 'description', - value: 'a description', - actionType: 'append', - pipes: ['myTestPipe', 'append'], - }, - ]); - }); -}); - -describe('transformFields', () => { - test('transform fields for creation correctly', () => { - const fields = prepareFieldsForTransformation({ - externalCase: fullParams.externalObject, - mapping: finalMapping, - }); - - const res = transformFields< - PushToServiceApiParams, - {}, - { short_description: string; description: string } - >({ - params: fullParams, - fields, - }); - - expect(res).toEqual({ - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - }); - - test('transform fields for update correctly', () => { - const fields = prepareFieldsForTransformation({ - externalCase: fullParams.externalObject, - mapping: finalMapping, - defaultPipes: ['informationUpdated'], - }); - - const res = transformFields< - PushToServiceApiParams, - {}, - { short_description: string; description: string } - >({ - params: { - ...fullParams, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { - username: 'anotherUser', - fullName: 'Another User', - }, - }, - fields, - currentIncident: { - short_description: 'first title (created at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - }); - expect(res).toEqual({ - short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)', - description: - 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User) \r\na description (updated at 2020-03-15T08:34:53.450Z by Another User)', - }); - }); - - test('add newline character to description', () => { - const fields = prepareFieldsForTransformation({ - externalCase: fullParams.externalObject, - mapping: finalMapping, - defaultPipes: ['informationUpdated'], - }); - - const res = transformFields< - PushToServiceApiParams, - {}, - { short_description: string; description: string } - >({ - params: fullParams, - fields, - currentIncident: { - short_description: 'first title', - description: 'first description', - }, - }); - expect(res.description?.includes('\r\n')).toBe(true); - }); - - test('append username if fullname is undefined when create', () => { - const fields = prepareFieldsForTransformation({ - externalCase: fullParams.externalObject, - mapping: finalMapping, - }); - - const res = transformFields< - PushToServiceApiParams, - {}, - { short_description: string; description: string } - >({ - params: { - ...fullParams, - createdBy: { fullName: '', username: 'elastic' }, - }, - fields, - }); - - expect(res).toEqual({ - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by elastic)', - description: 'a description (created at 2020-03-13T08:34:53.450Z by elastic)', - }); - }); - - test('append username if fullname is undefined when update', () => { - const fields = prepareFieldsForTransformation({ - externalCase: fullParams.externalObject, - mapping: finalMapping, - defaultPipes: ['informationUpdated'], - }); - - const res = transformFields< - PushToServiceApiParams, - {}, - { short_description: string; description: string } - >({ - params: { - ...fullParams, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { username: 'anotherUser', fullName: '' }, - }, - fields, - }); - - expect(res).toEqual({ - short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by anotherUser)', - description: 'a description (updated at 2020-03-15T08:34:53.450Z by anotherUser)', - }); - }); -}); - -describe('transformComments', () => { - test('transform creation comments', () => { - const comments: Comment[] = [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ]; - const res = transformComments(comments, ['informationCreated']); - expect(res).toEqual([ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - comment: 'first comment (created at 2020-03-13T08:34:53.450Z by Elastic User)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ]); - }); - - test('transform update comments', () => { - const comments: Comment[] = [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { - fullName: 'Another User', - username: 'anotherUser', - }, - }, - ]; - const res = transformComments(comments, ['informationUpdated']); - expect(res).toEqual([ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - comment: 'first comment (updated at 2020-03-15T08:34:53.450Z by Another User)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { - fullName: 'Another User', - username: 'anotherUser', - }, - }, - ]); - }); - - test('transform added comments', () => { - const comments: Comment[] = [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ]; - const res = transformComments(comments, ['informationAdded']); - expect(res).toEqual([ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ]); - }); - - test('transform comments without fullname', () => { - const comments: Comment[] = [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: '', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ]; - const res = transformComments(comments, ['informationAdded']); - expect(res).toEqual([ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - comment: 'first comment (added at 2020-03-13T08:34:53.450Z by elastic)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: '', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ]); - }); - - test('adds update user correctly', () => { - const comments: Comment[] = [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic', username: 'elastic' }, - updatedAt: '2020-04-13T08:34:53.450Z', - updatedBy: { fullName: 'Elastic2', username: 'elastic' }, - }, - ]; - const res = transformComments(comments, ['informationAdded']); - expect(res).toEqual([ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - comment: 'first comment (added at 2020-04-13T08:34:53.450Z by Elastic2)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic', username: 'elastic' }, - updatedAt: '2020-04-13T08:34:53.450Z', - updatedBy: { fullName: 'Elastic2', username: 'elastic' }, - }, - ]); - }); - - test('adds update user with empty fullname correctly', () => { - const comments: Comment[] = [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic', username: 'elastic' }, - updatedAt: '2020-04-13T08:34:53.450Z', - updatedBy: { fullName: '', username: 'elastic2' }, - }, - ]; - const res = transformComments(comments, ['informationAdded']); - expect(res).toEqual([ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - comment: 'first comment (added at 2020-04-13T08:34:53.450Z by elastic2)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic', username: 'elastic' }, - updatedAt: '2020-04-13T08:34:53.450Z', - updatedBy: { fullName: '', username: 'elastic2' }, - }, - ]); - }); -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts deleted file mode 100644 index 3d51f5e826279..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { flow, get } from 'lodash'; - -import { - MapRecord, - TransformFieldsArgs, - Comment, - EntityInformation, - PipedField, - AnyParams, - PrepareFieldsForTransformArgs, -} from './types'; - -import { transformers } from './transformers'; - -import { SUPPORTED_SOURCE_FIELDS } from './constants'; - -export const normalizeMapping = (supportedFields: string[], mapping: MapRecord[]): MapRecord[] => { - // Prevent prototype pollution and remove unsupported fields - return mapping.filter( - (m) => - m.source !== '__proto__' && m.target !== '__proto__' && supportedFields.includes(m.source) - ); -}; - -export const buildMap = (mapping: MapRecord[]): Map => { - return normalizeMapping(SUPPORTED_SOURCE_FIELDS, mapping).reduce((fieldsMap, field) => { - const { source, target, actionType } = field; - fieldsMap.set(source, { target, actionType }); - fieldsMap.set(target, { target: source, actionType }); - return fieldsMap; - }, new Map()); -}; - -export const mapParams = (params: T, mapping: Map): AnyParams => { - return Object.keys(params).reduce((prev: AnyParams, curr: string): AnyParams => { - const field = mapping.get(curr); - if (field) { - prev[field.target] = get(params, curr); - } - return prev; - }, {}); -}; - -export const prepareFieldsForTransformation = ({ - externalCase, - mapping, - defaultPipes = ['informationCreated'], -}: PrepareFieldsForTransformArgs): PipedField[] => { - return Object.keys(externalCase) - .filter((p) => mapping.get(p)?.actionType != null && mapping.get(p)?.actionType !== 'nothing') - .map((p) => { - const actionType = mapping.get(p)?.actionType ?? 'nothing'; - return { - key: p, - value: externalCase[p], - actionType, - pipes: actionType === 'append' ? [...defaultPipes, 'append'] : defaultPipes, - }; - }); -}; - -export const transformFields = < - P extends EntityInformation, - S extends Record, - R extends {} ->({ - params, - fields, - currentIncident, -}: TransformFieldsArgs): R => { - return fields.reduce((prev, cur) => { - const transform = flow(...cur.pipes.map((p) => transformers[p])); - return { - ...prev, - [cur.key]: transform({ - value: cur.value, - date: params.updatedAt ?? params.createdAt, - user: getEntity(params), - previousValue: currentIncident ? currentIncident[cur.key] : '', - }).value, - }; - }, {} as R); -}; - -export const transformComments = (comments: Comment[], pipes: string[]): Comment[] => { - return comments.map((c) => ({ - ...c, - comment: flow(...pipes.map((p) => transformers[p]))({ - value: c.comment, - date: c.updatedAt ?? c.createdAt, - user: getEntity(c), - }).value, - })); -}; - -export const getEntity = (entity: EntityInformation): string => - (entity.updatedBy != null - ? entity.updatedBy.fullName - ? entity.updatedBy.fullName - : entity.updatedBy.username - : entity.createdBy != null - ? entity.createdBy.fullName - ? entity.createdBy.fullName - : entity.createdBy.username - : '') ?? ''; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts index 5a7617ada1bf0..99021c1fc552b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts @@ -5,7 +5,7 @@ */ import { Logger } from '../../../../../../src/core/server'; -import { externalServiceMock, mapping, apiParams } from './mocks'; +import { externalServiceMock, apiParams } from './mocks'; import { ExternalService } from './types'; import { api } from './api'; let mockedLogger: jest.Mocked; @@ -22,7 +22,6 @@ describe('api', () => { const params = { ...apiParams, externalId: null }; const res = await api.pushToService({ externalService, - mapping, params, logger: mockedLogger, }); @@ -49,7 +48,6 @@ describe('api', () => { const params = { ...apiParams, externalId: null, comments: [] }; const res = await api.pushToService({ externalService, - mapping, params, logger: mockedLogger, }); @@ -63,8 +61,8 @@ describe('api', () => { }); test('it calls createIncident correctly', async () => { - const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; + await api.pushToService({ externalService, params, logger: mockedLogger }); expect(externalService.createIncident).toHaveBeenCalledWith({ incident: { @@ -72,16 +70,16 @@ describe('api', () => { priority: 'High', issueType: '10006', parent: null, - description: 'Incident description (created at 2020-04-27T10:59:46.202Z by Elastic User)', - summary: 'Incident title (created at 2020-04-27T10:59:46.202Z by Elastic User)', + description: 'Incident description', + summary: 'Incident title', }, }); expect(externalService.updateIncident).not.toHaveBeenCalled(); }); test('it calls createIncident correctly without mapping', async () => { - const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; + await api.pushToService({ externalService, params, logger: mockedLogger }); expect(externalService.createIncident).toHaveBeenCalledWith({ incident: { @@ -97,24 +95,14 @@ describe('api', () => { }); test('it calls createComment correctly', async () => { - const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; + await api.pushToService({ externalService, params, logger: mockedLogger }); expect(externalService.createComment).toHaveBeenCalledTimes(2); expect(externalService.createComment).toHaveBeenNthCalledWith(1, { incidentId: 'incident-1', comment: { commentId: 'case-comment-1', - comment: 'A comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', - createdAt: '2020-04-27T10:59:46.202Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-04-27T10:59:46.202Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + comment: 'A comment', }, }); @@ -122,40 +110,20 @@ describe('api', () => { incidentId: 'incident-1', comment: { commentId: 'case-comment-2', - comment: 'Another comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', - createdAt: '2020-04-27T10:59:46.202Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-04-27T10:59:46.202Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + comment: 'Another comment', }, }); }); test('it calls createComment correctly without mapping', async () => { - const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; + await api.pushToService({ externalService, params, logger: mockedLogger }); expect(externalService.createComment).toHaveBeenCalledTimes(2); expect(externalService.createComment).toHaveBeenNthCalledWith(1, { incidentId: 'incident-1', comment: { commentId: 'case-comment-1', comment: 'A comment', - createdAt: '2020-04-27T10:59:46.202Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-04-27T10:59:46.202Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, }, }); @@ -164,16 +132,6 @@ describe('api', () => { comment: { commentId: 'case-comment-2', comment: 'Another comment', - createdAt: '2020-04-27T10:59:46.202Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-04-27T10:59:46.202Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, }, }); }); @@ -183,7 +141,6 @@ describe('api', () => { test('it updates an incident', async () => { const res = await api.pushToService({ externalService, - mapping, params: apiParams, logger: mockedLogger, }); @@ -210,7 +167,6 @@ describe('api', () => { const params = { ...apiParams, comments: [] }; const res = await api.pushToService({ externalService, - mapping, params, logger: mockedLogger, }); @@ -225,7 +181,7 @@ describe('api', () => { test('it calls updateIncident correctly', async () => { const params = { ...apiParams }; - await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); + await api.pushToService({ externalService, params, logger: mockedLogger }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', @@ -234,8 +190,8 @@ describe('api', () => { priority: 'High', issueType: '10006', parent: null, - description: 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', - summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + description: 'Incident description', + summary: 'Incident title', }, }); expect(externalService.createIncident).not.toHaveBeenCalled(); @@ -243,7 +199,7 @@ describe('api', () => { test('it calls updateIncident correctly without mapping', async () => { const params = { ...apiParams }; - await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); + await api.pushToService({ externalService, params, logger: mockedLogger }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', @@ -261,23 +217,13 @@ describe('api', () => { test('it calls createComment correctly', async () => { const params = { ...apiParams }; - await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); + await api.pushToService({ externalService, params, logger: mockedLogger }); expect(externalService.createComment).toHaveBeenCalledTimes(2); expect(externalService.createComment).toHaveBeenNthCalledWith(1, { incidentId: 'incident-1', comment: { commentId: 'case-comment-1', - comment: 'A comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', - createdAt: '2020-04-27T10:59:46.202Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-04-27T10:59:46.202Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + comment: 'A comment', }, }); @@ -285,40 +231,20 @@ describe('api', () => { incidentId: 'incident-1', comment: { commentId: 'case-comment-2', - comment: 'Another comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', - createdAt: '2020-04-27T10:59:46.202Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-04-27T10:59:46.202Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + comment: 'Another comment', }, }); }); test('it calls createComment correctly without mapping', async () => { const params = { ...apiParams }; - await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); + await api.pushToService({ externalService, params, logger: mockedLogger }); expect(externalService.createComment).toHaveBeenCalledTimes(2); expect(externalService.createComment).toHaveBeenNthCalledWith(1, { incidentId: 'incident-1', comment: { commentId: 'case-comment-1', comment: 'A comment', - createdAt: '2020-04-27T10:59:46.202Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-04-27T10:59:46.202Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, }, }); @@ -327,16 +253,6 @@ describe('api', () => { comment: { commentId: 'case-comment-2', comment: 'Another comment', - createdAt: '2020-04-27T10:59:46.202Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-04-27T10:59:46.202Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, }, }); }); @@ -411,396 +327,4 @@ describe('api', () => { }); }); }); - - describe('mapping variations', () => { - test('overwrite & append', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'append', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, - summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', - description: - 'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', - }, - }); - }); - - test('nothing & append', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'nothing', - }); - - mapping.set('description', { - target: 'description', - actionType: 'append', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'nothing', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, - description: - 'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', - }, - }); - }); - - test('append & append', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'append', - }); - - mapping.set('description', { - target: 'description', - actionType: 'append', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'append', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, - summary: - 'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', - description: - 'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', - }, - }); - }); - - test('nothing & nothing', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'nothing', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'nothing', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, - }, - }); - }); - - test('overwrite & nothing', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, - summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', - }, - }); - }); - - test('overwrite & overwrite', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, - summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', - description: 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', - }, - }); - }); - - test('nothing & overwrite', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'nothing', - }); - - mapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'nothing', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, - description: 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', - }, - }); - }); - - test('append & overwrite', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'append', - }); - - mapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'append', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, - summary: - 'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', - description: 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', - }, - }); - }); - - test('append & nothing', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'append', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'append', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, - summary: - 'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', - }, - }); - }); - - test('comment nothing', async () => { - mapping.set('title', { - target: 'summary', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'nothing', - }); - - mapping.set('summary', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.createComment).not.toHaveBeenCalled(); - }); - }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts index feeb69b1d1a0e..cd0d410bd8dfa 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -5,7 +5,6 @@ */ import { - ExternalServiceParams, PushToServiceApiHandlerArgs, HandshakeApiHandlerArgs, GetIncidentApiHandlerArgs, @@ -14,26 +13,17 @@ import { GetFieldsByIssueTypeHandlerArgs, GetIssueTypesHandlerArgs, GetIssuesHandlerArgs, - PushToServiceApiParams, PushToServiceResponse, GetIssueHandlerArgs, GetCommonFieldsHandlerArgs, } from './types'; -// TODO: to remove, need to support Case -import { prepareFieldsForTransformation, transformFields, transformComments } from '../case/utils'; +const handshakeHandler = async ({ externalService, params }: HandshakeApiHandlerArgs) => {}; -const handshakeHandler = async ({ - externalService, - mapping, - params, -}: HandshakeApiHandlerArgs) => {}; - -const getIncidentHandler = async ({ - externalService, - mapping, - params, -}: GetIncidentApiHandlerArgs) => {}; +const getIncidentHandler = async ({ externalService, params }: GetIncidentApiHandlerArgs) => { + const res = await externalService.getIncident(params.externalId); + return res; +}; const getIssueTypesHandler = async ({ externalService }: GetIssueTypesHandlerArgs) => { const res = await externalService.getIssueTypes(); @@ -68,58 +58,12 @@ const getIssueHandler = async ({ externalService, params }: GetIssueHandlerArgs) const pushToServiceHandler = async ({ externalService, - mapping, params, - logger, }: PushToServiceApiHandlerArgs): Promise => { - const { externalId, comments } = params; - const updateIncident = externalId ? true : false; - const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated']; - let currentIncident: ExternalServiceParams | undefined; + const { comments } = params; let res: PushToServiceResponse; - - if (externalId) { - try { - currentIncident = await externalService.getIncident(externalId); - } catch (ex) { - logger.debug( - `Retrieving Incident by id ${externalId} from Jira failed with exception: ${ex}` - ); - } - } - - let incident: Incident; - // TODO: should be removed later but currently keep it for the Case implementation support - if (mapping) { - const fields = prepareFieldsForTransformation({ - externalCase: params.externalObject, - mapping, - defaultPipes, - }); - - const transformedFields = transformFields< - PushToServiceApiParams, - ExternalServiceParams, - Incident - >({ - params, - fields, - currentIncident, - }); - - const { priority, labels, issueType, parent } = params; - incident = { - summary: transformedFields.summary, - description: transformedFields.description, - priority, - labels, - issueType, - parent, - }; - } else { - const { title, description, priority, labels, issueType, parent } = params; - incident = { summary: title, description, priority, labels, issueType, parent }; - } + const { externalId, ...rest } = params.incident; + const incident: Incident = rest; if (externalId != null) { res = await externalService.updateIncident({ @@ -128,23 +72,13 @@ const pushToServiceHandler = async ({ }); } else { res = await externalService.createIncident({ - incident: { - ...incident, - }, + incident, }); } if (comments && Array.isArray(comments) && comments.length > 0) { - if (mapping && mapping.get('comments')?.actionType === 'nothing') { - return res; - } - - const commentsTransformed = mapping - ? transformComments(comments, ['informationAdded']) - : comments; - res.comments = []; - for (const currentComment of commentsTransformed) { + for (const currentComment of comments) { const comment = await externalService.createComment({ incidentId: res.id, comment: currentComment, 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 c70c0810926f4..4518fa0f119d5 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 @@ -27,13 +27,11 @@ import { ExecutorSubActionGetIssueTypesParams, ExecutorSubActionGetIssuesParams, ExecutorSubActionGetIssueParams, + ExecutorSubActionGetIncidentParams, } from './types'; import * as i18n from './translations'; import { Logger } from '../../../../../../src/core/server'; -// TODO: to remove, need to support Case -import { buildMap, mapParams } from '../case/utils'; - interface GetActionTypeParams { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; @@ -41,6 +39,7 @@ interface GetActionTypeParams { const supportedSubActions: string[] = [ 'getFields', + 'getIncident', 'pushToService', 'issueTypes', 'fieldsByIssueType', @@ -109,21 +108,22 @@ async function executor( throw new Error(errorMessage); } + if (subAction === 'getIncident') { + const getIncidentParams = subActionParams as ExecutorSubActionGetIncidentParams; + const res = await api.getIncident({ + externalService, + params: getIncidentParams, + }); + if (res != null) { + data = res; + } + } if (subAction === 'pushToService') { const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; - const { comments, externalId, ...restParams } = pushToServiceParams; - const incidentConfiguration = config.incidentConfiguration; - const mapping = incidentConfiguration ? buildMap(incidentConfiguration.mapping) : null; - const externalObject = - config.incidentConfiguration && mapping - ? mapParams(restParams as ExecutorSubActionPushParams, mapping) - : {}; - data = await api.pushToService({ externalService, - mapping, - params: { ...pushToServiceParams, externalObject }, + params: pushToServiceParams, logger, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts index 87a0f156a0c2a..cc37dd475f42c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts @@ -6,8 +6,6 @@ import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types'; -import { MapRecord } from '../case/types'; - const createMock = (): jest.Mocked => { const service = { getIncident: jest.fn().mockImplementation(() => @@ -111,64 +109,31 @@ const createMock = (): jest.Mocked => { const externalServiceMock = { create: createMock, }; -const mapping: Map> = new Map(); - -mapping.set('title', { - target: 'summary', - actionType: 'overwrite', -}); - -mapping.set('description', { - target: 'description', - actionType: 'overwrite', -}); - -mapping.set('comments', { - target: 'comments', - actionType: 'append', -}); - -mapping.set('summary', { - target: 'title', - actionType: 'overwrite', -}); const executorParams: ExecutorSubActionPushParams = { - savedObjectId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', - externalId: 'incident-3', - createdAt: '2020-04-27T10:59:46.202Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-04-27T10:59:46.202Z', - updatedBy: { fullName: 'Elastic User', username: 'elastic' }, - title: 'Incident title', - description: 'Incident description', - labels: ['kibana', 'elastic'], - priority: 'High', - issueType: '10006', - parent: null, + incident: { + externalId: 'incident-3', + summary: 'Incident title', + description: 'Incident description', + labels: ['kibana', 'elastic'], + priority: 'High', + issueType: '10006', + parent: null, + }, comments: [ { commentId: 'case-comment-1', comment: 'A comment', - createdAt: '2020-04-27T10:59:46.202Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-04-27T10:59:46.202Z', - updatedBy: { fullName: 'Elastic User', username: 'elastic' }, }, { commentId: 'case-comment-2', comment: 'Another comment', - createdAt: '2020-04-27T10:59:46.202Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-04-27T10:59:46.202Z', - updatedBy: { fullName: 'Elastic User', username: 'elastic' }, }, ], }; const apiParams: PushToServiceApiParams = { ...executorParams, - externalObject: { summary: 'Incident title', description: 'Incident description' }, }; -export { externalServiceMock, mapping, executorParams, apiParams }; +export { externalServiceMock, executorParams, apiParams }; 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 70b60ada9c386..1885e64bbe329 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 @@ -5,14 +5,10 @@ */ import { schema } from '@kbn/config-schema'; -import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from '../case/schema'; export const ExternalIncidentServiceConfiguration = { apiUrl: schema.string(), projectKey: schema.string(), - // TODO: to remove - set it optional for the current stage to support Case Jira implementation - incidentConfiguration: schema.nullable(IncidentConfigurationSchema), - isCaseOwned: schema.nullable(schema.boolean()), }; export const ExternalIncidentServiceConfigurationSchema = schema.object( @@ -37,17 +33,23 @@ export const ExecutorSubActionSchema = schema.oneOf([ ]); export const ExecutorSubActionPushParamsSchema = schema.object({ - savedObjectId: schema.nullable(schema.string()), - title: schema.string(), - description: schema.nullable(schema.string()), - externalId: schema.nullable(schema.string()), - issueType: schema.nullable(schema.string()), - priority: schema.nullable(schema.string()), - labels: schema.nullable(schema.arrayOf(schema.string())), - parent: schema.nullable(schema.string()), - // TODO: modify later to string[] - need for support Case schema - comments: schema.nullable(schema.arrayOf(CommentSchema)), - ...EntityInformation, + incident: schema.object({ + summary: schema.string(), + description: schema.nullable(schema.string()), + externalId: schema.nullable(schema.string()), + issueType: schema.nullable(schema.string()), + priority: schema.nullable(schema.string()), + labels: schema.nullable(schema.arrayOf(schema.string())), + parent: schema.nullable(schema.string()), + }), + comments: schema.nullable( + schema.arrayOf( + schema.object({ + comment: schema.string(), + commentId: schema.string(), + }) + ) + ), }); export const ExecutorSubActionGetIncidentParamsSchema = schema.object({ 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 c7d0153daec24..30144416491dd 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 @@ -479,10 +479,6 @@ describe('Jira service', () => { comment: { comment: 'comment', commentId: 'comment-1', - createdBy: null, - createdAt: null, - updatedAt: null, - updatedBy: null, }, }); @@ -507,10 +503,6 @@ describe('Jira service', () => { comment: { comment: 'comment', commentId: 'comment-1', - createdBy: null, - createdAt: null, - updatedAt: null, - updatedBy: null, }, }); @@ -536,10 +528,6 @@ describe('Jira service', () => { comment: { comment: 'comment', commentId: 'comment-1', - createdBy: null, - createdAt: null, - updatedAt: null, - updatedBy: null, }, }) ).rejects.toThrow( diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index 742e68eccbb23..f507893365c8a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -133,21 +133,24 @@ export const createExternalService = ( [key: string]: { allowedValues?: Array<{}>; defaultValue?: {}; + name: string; required: boolean; schema: FieldSchema; }; }) => - Object.keys(fields ?? {}).reduce((fieldsAcc, fieldKey) => { - return { + Object.keys(fields ?? {}).reduce( + (fieldsAcc, fieldKey) => ({ ...fieldsAcc, [fieldKey]: { required: fields[fieldKey]?.required, allowedValues: fields[fieldKey]?.allowedValues ?? [], defaultValue: fields[fieldKey]?.defaultValue ?? {}, schema: fields[fieldKey]?.schema, + name: fields[fieldKey]?.name, }, - }; - }, {}); + }), + {} + ); const normalizeSearchResults = ( issues: Array<{ id: string; key: string; fields: { summary: string } }> @@ -386,7 +389,6 @@ export const createExternalService = ( }); const fields = res.data.projects[0]?.issuetypes[0]?.fields || {}; - return normalizeFields(fields); } else { const res = await request({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts index 0e71de813eb5d..196799df7599a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts @@ -17,11 +17,3 @@ export const ALLOWED_HOSTS_ERROR = (message: string) => message, }, }); - -// TODO: remove when Case mappings will be removed -export const MAPPING_EMPTY = i18n.translate( - 'xpack.actions.builtin.jira.configuration.emptyMapping', - { - defaultMessage: '[incidentConfiguration.mapping]: expected non-empty but got empty', - } -); 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 e142637010a98..6c72ce3cde499 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 @@ -21,8 +21,6 @@ import { ExecutorSubActionGetIssueParamsSchema, } from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; -import { IncidentConfigurationSchema } from '../case/schema'; -import { Comment } from '../case/types'; import { Logger } from '../../../../../../src/core/server'; export type JiraPublicConfigurationType = TypeOf; @@ -33,8 +31,6 @@ export type JiraSecretConfigurationType = TypeOf< export type ExecutorParams = TypeOf; export type ExecutorSubActionPushParams = TypeOf; -export type IncidentConfiguration = TypeOf; - export interface ExternalServiceCredentials { config: Record; secrets: Record; @@ -52,18 +48,9 @@ export interface ExternalServiceIncidentResponse { pushedDate: string; } -export interface ExternalServiceCommentResponse { - commentId: string; - pushedDate: string; - externalCommentId?: string; -} - export type ExternalServiceParams = Record; -export type Incident = Pick< - ExecutorSubActionPushParams, - 'description' | 'priority' | 'labels' | 'issueType' | 'parent' -> & { summary: string }; +export type Incident = Omit; export interface CreateIncidentParams { incident: Incident; @@ -76,7 +63,7 @@ export interface UpdateIncidentParams { export interface CreateCommentParams { incidentId: string; - comment: Comment; + comment: SimpleComment; } export interface FieldsSchema { @@ -84,18 +71,6 @@ export interface FieldsSchema { [key: string]: string; } -export interface ExternalServiceFields { - clauseNames: string[]; - custom: boolean; - id: string; - key: string; - name: string; - navigatable: boolean; - orderable: boolean; - schema: FieldsSchema; - searchable: boolean; -} - export type GetIssueTypesResponse = Array<{ id: string; name: string }>; export interface FieldSchema { @@ -104,7 +79,13 @@ export interface FieldSchema { } export type GetFieldsByIssueTypeResponse = Record< string, - { allowedValues: Array<{}>; defaultValue: {}; required: boolean; schema: FieldSchema } + { + allowedValues: Array<{}>; + defaultValue: {}; + required: boolean; + schema: FieldSchema; + name: string; + } >; export type GetCommonFieldsResponse = GetFieldsByIssueTypeResponse; @@ -128,9 +109,7 @@ export interface ExternalService { updateIncident: (params: UpdateIncidentParams) => Promise; } -export interface PushToServiceApiParams extends ExecutorSubActionPushParams { - externalObject: Record; -} +export type PushToServiceApiParams = ExecutorSubActionPushParams; export type ExecutorSubActionGetIncidentParams = TypeOf< typeof ExecutorSubActionGetIncidentParamsSchema @@ -160,7 +139,6 @@ export type ExecutorSubActionGetIssueParams = TypeOf | null; } export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { @@ -207,7 +185,7 @@ export interface GetIssueHandlerArgs { export interface ExternalServiceApi { getFields: (args: GetCommonFieldsHandlerArgs) => Promise; - getIncident: (args: GetIncidentApiHandlerArgs) => Promise; + getIncident: (args: GetIncidentApiHandlerArgs) => Promise; handshake: (args: HandshakeApiHandlerArgs) => Promise; issueTypes: (args: GetIssueTypesHandlerArgs) => Promise; pushToService: (args: PushToServiceApiHandlerArgs) => Promise; @@ -223,7 +201,8 @@ export type JiraExecutorResultData = | GetIssueTypesResponse | GetFieldsByIssueTypeResponse | GetIssuesResponse - | GetIssueResponse; + | GetIssueResponse + | ExternalServiceParams; export interface Fields { [key: string]: string | string[] | { name: string } | { key: string } | { id: string }; @@ -232,3 +211,12 @@ export interface ResponseError { errorMessages: string[] | null | undefined; errors: { [k: string]: string } | null | undefined; } +export interface SimpleComment { + comment: string; + commentId: string; +} +export interface ExternalServiceCommentResponse { + commentId: string; + pushedDate: string; + externalCommentId?: string; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts index 58a3e27247fae..2e4d3e56c4102 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty } from 'lodash'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { JiraPublicConfigurationType, @@ -18,13 +17,6 @@ export const validateCommonConfig = ( configurationUtilities: ActionsConfigurationUtilities, configObject: JiraPublicConfigurationType ) => { - if ( - configObject.incidentConfiguration !== null && - isEmpty(configObject.incidentConfiguration.mapping) - ) { - return i18n.MAPPING_EMPTY; - } - try { configurationUtilities.ensureUriAllowed(configObject.apiUrl); } catch (allowedListError) { diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.test.ts index 0892a2789bbc0..5c018fe748c6c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.test.ts @@ -6,7 +6,7 @@ import { Logger } from '../../../../../../src/core/server'; import { api } from './api'; -import { externalServiceMock, mapping, apiParams } from './mocks'; +import { externalServiceMock, apiParams } from './mocks'; import { ExternalService } from './types'; let mockedLogger: jest.Mocked; @@ -28,7 +28,6 @@ describe('api', () => { const params = { ...apiParams, externalId: null }; const res = await api.pushToService({ externalService, - mapping, params, logger: mockedLogger, }); @@ -55,7 +54,6 @@ describe('api', () => { const params = { ...apiParams, externalId: null, comments: [] }; const res = await api.pushToService({ externalService, - mapping, params, logger: mockedLogger, }); @@ -69,16 +67,15 @@ describe('api', () => { }); test('it calls createIncident correctly', async () => { - const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; + await api.pushToService({ externalService, params, logger: mockedLogger }); expect(externalService.createIncident).toHaveBeenCalledWith({ incident: { incidentTypes: [1001], severityCode: 6, - description: - 'Incident description (created at 2020-06-03T15:09:13.606Z by Elastic User)', - name: 'Incident title (created at 2020-06-03T15:09:13.606Z by Elastic User)', + description: 'Incident description', + name: 'Incident title', }, }); expect(externalService.updateIncident).not.toHaveBeenCalled(); @@ -86,23 +83,13 @@ describe('api', () => { test('it calls createComment correctly', async () => { const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); + await api.pushToService({ externalService, params, logger: mockedLogger }); expect(externalService.createComment).toHaveBeenCalledTimes(2); expect(externalService.createComment).toHaveBeenNthCalledWith(1, { incidentId: '1', comment: { commentId: 'case-comment-1', - comment: 'A comment (added at 2020-06-03T15:09:13.606Z by Elastic User)', - createdAt: '2020-06-03T15:09:13.606Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-06-03T15:09:13.606Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + comment: 'A comment', }, }); @@ -110,17 +97,7 @@ describe('api', () => { incidentId: '1', comment: { commentId: 'case-comment-2', - comment: 'Another comment (added at 2020-06-03T15:09:13.606Z by Elastic User)', - createdAt: '2020-06-03T15:09:13.606Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-06-03T15:09:13.606Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + comment: 'Another comment', }, }); }); @@ -130,7 +107,6 @@ describe('api', () => { test('it updates an incident', async () => { const res = await api.pushToService({ externalService, - mapping, params: apiParams, logger: mockedLogger, }); @@ -157,7 +133,6 @@ describe('api', () => { const params = { ...apiParams, comments: [] }; const res = await api.pushToService({ externalService, - mapping, params, logger: mockedLogger, }); @@ -172,16 +147,15 @@ describe('api', () => { test('it calls updateIncident correctly', async () => { const params = { ...apiParams }; - await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); + await api.pushToService({ externalService, params, logger: mockedLogger }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { incidentTypes: [1001], severityCode: 6, - description: - 'Incident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)', - name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)', + description: 'Incident description', + name: 'Incident title', }, }); expect(externalService.createIncident).not.toHaveBeenCalled(); @@ -189,23 +163,13 @@ describe('api', () => { test('it calls createComment correctly', async () => { const params = { ...apiParams }; - await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); + await api.pushToService({ externalService, params, logger: mockedLogger }); expect(externalService.createComment).toHaveBeenCalledTimes(2); expect(externalService.createComment).toHaveBeenNthCalledWith(1, { incidentId: '1', comment: { commentId: 'case-comment-1', - comment: 'A comment (added at 2020-06-03T15:09:13.606Z by Elastic User)', - createdAt: '2020-06-03T15:09:13.606Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-06-03T15:09:13.606Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + comment: 'A comment', }, }); @@ -213,17 +177,7 @@ describe('api', () => { incidentId: '1', comment: { commentId: 'case-comment-2', - comment: 'Another comment (added at 2020-06-03T15:09:13.606Z by Elastic User)', - createdAt: '2020-06-03T15:09:13.606Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-06-03T15:09:13.606Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + comment: 'Another comment', }, }); }); @@ -236,14 +190,8 @@ describe('api', () => { params: {}, }); expect(res).toEqual([ - { - id: 17, - name: 'Communication error (fax; email)', - }, - { - id: 1001, - name: 'Custom type', - }, + { id: 17, name: 'Communication error (fax; email)' }, + { id: 1001, name: 'Custom type' }, ]); }); }); @@ -255,397 +203,11 @@ describe('api', () => { params: { id: '10006' }, }); expect(res).toEqual([ - { - id: 4, - name: 'Low', - }, - { - id: 5, - name: 'Medium', - }, - { - id: 6, - name: 'High', - }, + { id: 4, name: 'Low' }, + { id: 5, name: 'Medium' }, + { id: 6, name: 'High' }, ]); }); }); - - describe('mapping variations', () => { - test('overwrite & append', async () => { - mapping.set('title', { - target: 'name', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'append', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('name', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - incidentTypes: [1001], - severityCode: 6, - name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)', - description: - 'description from ibm resilient \r\nIncident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)', - }, - }); - }); - - test('nothing & append', async () => { - mapping.set('title', { - target: 'name', - actionType: 'nothing', - }); - - mapping.set('description', { - target: 'description', - actionType: 'append', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('name', { - target: 'title', - actionType: 'nothing', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - incidentTypes: [1001], - severityCode: 6, - description: - 'description from ibm resilient \r\nIncident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)', - }, - }); - }); - - test('append & append', async () => { - mapping.set('title', { - target: 'name', - actionType: 'append', - }); - - mapping.set('description', { - target: 'description', - actionType: 'append', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('name', { - target: 'title', - actionType: 'append', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - incidentTypes: [1001], - severityCode: 6, - name: - 'title from ibm resilient \r\nIncident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)', - description: - 'description from ibm resilient \r\nIncident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)', - }, - }); - }); - - test('nothing & nothing', async () => { - mapping.set('title', { - target: 'name', - actionType: 'nothing', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('name', { - target: 'title', - actionType: 'nothing', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - incidentTypes: [1001], - severityCode: 6, - }, - }); - }); - - test('overwrite & nothing', async () => { - mapping.set('title', { - target: 'name', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('name', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - incidentTypes: [1001], - severityCode: 6, - name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)', - }, - }); - }); - - test('overwrite & overwrite', async () => { - mapping.set('title', { - target: 'name', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('name', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - incidentTypes: [1001], - severityCode: 6, - name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)', - description: - 'Incident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)', - }, - }); - }); - - test('nothing & overwrite', async () => { - mapping.set('title', { - target: 'name', - actionType: 'nothing', - }); - - mapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('name', { - target: 'title', - actionType: 'nothing', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - incidentTypes: [1001], - severityCode: 6, - description: - 'Incident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)', - }, - }); - }); - - test('append & overwrite', async () => { - mapping.set('title', { - target: 'name', - actionType: 'append', - }); - - mapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('name', { - target: 'title', - actionType: 'append', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - incidentTypes: [1001], - severityCode: 6, - name: - 'title from ibm resilient \r\nIncident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)', - description: - 'Incident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)', - }, - }); - }); - - test('append & nothing', async () => { - mapping.set('title', { - target: 'name', - actionType: 'append', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('name', { - target: 'title', - actionType: 'append', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - incidentTypes: [1001], - severityCode: 6, - name: - 'title from ibm resilient \r\nIncident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)', - }, - }); - }); - - test('comment nothing', async () => { - mapping.set('title', { - target: 'name', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'nothing', - }); - - mapping.set('name', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - logger: mockedLogger, - }); - expect(externalService.createComment).not.toHaveBeenCalled(); - }); - }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts index 29f2594d2b6f8..b308df1444c93 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts @@ -5,7 +5,6 @@ */ import { - ExternalServiceParams, PushToServiceApiHandlerArgs, HandshakeApiHandlerArgs, GetIncidentApiHandlerArgs, @@ -13,25 +12,13 @@ import { Incident, GetIncidentTypesHandlerArgs, GetSeverityHandlerArgs, - PushToServiceApiParams, PushToServiceResponse, GetCommonFieldsHandlerArgs, } from './types'; -// TODO: to remove, need to support Case -import { transformFields, prepareFieldsForTransformation, transformComments } from '../case/utils'; +const handshakeHandler = async ({ externalService, params }: HandshakeApiHandlerArgs) => {}; -const handshakeHandler = async ({ - externalService, - mapping, - params, -}: HandshakeApiHandlerArgs) => {}; - -const getIncidentHandler = async ({ - externalService, - mapping, - params, -}: GetIncidentApiHandlerArgs) => {}; +const getIncidentHandler = async ({ externalService, params }: GetIncidentApiHandlerArgs) => {}; const getFieldsHandler = async ({ externalService }: GetCommonFieldsHandlerArgs) => { const res = await externalService.getFields(); @@ -49,56 +36,12 @@ const getSeverityHandler = async ({ externalService }: GetSeverityHandlerArgs) = const pushToServiceHandler = async ({ externalService, - mapping, params, - logger, }: PushToServiceApiHandlerArgs): Promise => { - const { externalId, comments } = params; - const updateIncident = externalId ? true : false; - const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated']; - let currentIncident: ExternalServiceParams | undefined; + const { comments } = params; let res: PushToServiceResponse; - - if (externalId) { - try { - currentIncident = await externalService.getIncident(externalId); - } catch (ex) { - logger.debug( - `Retrieving Incident by id ${externalId} from IBM Resilient was failed with exception: ${ex}` - ); - } - } - - let incident: Incident; - // TODO: should be removed later but currently keep it for the Case implementation support - if (mapping) { - const fields = prepareFieldsForTransformation({ - externalCase: params.externalObject, - mapping, - defaultPipes, - }); - - const transformedFields = transformFields< - PushToServiceApiParams, - ExternalServiceParams, - Incident - >({ - params, - fields, - currentIncident, - }); - - const { incidentTypes, severityCode } = params; - incident = { - name: transformedFields.name, - description: transformedFields.description, - incidentTypes, - severityCode, - }; - } else { - const { title, description, incidentTypes, severityCode } = params; - incident = { name: title, description, incidentTypes, severityCode }; - } + const { externalId, ...rest } = params.incident; + const incident: Incident = rest; if (externalId != null) { res = await externalService.updateIncident({ @@ -107,22 +50,13 @@ const pushToServiceHandler = async ({ }); } else { res = await externalService.createIncident({ - incident: { - ...incident, - }, + incident, }); } if (comments && Array.isArray(comments) && comments.length > 0) { - if (mapping && mapping.get('comments')?.actionType === 'nothing') { - return res; - } - const commentsTransformed = mapping - ? transformComments(comments, ['informationAdded']) - : comments; - res.comments = []; - for (const currentComment of commentsTransformed) { + for (const currentComment of comments) { const comment = await externalService.createComment({ incidentId: res.id, comment: currentComment, diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts index 6203dda4120f5..7ce9369289554 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts @@ -30,9 +30,6 @@ import { import * as i18n from './translations'; import { Logger } from '../../../../../../src/core/server'; -// TODO: to remove, need to support Case -import { buildMap, mapParams } from '../case/utils'; - interface GetActionTypeParams { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; @@ -104,19 +101,9 @@ async function executor( if (subAction === 'pushToService') { const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; - const { comments, externalId, ...restParams } = pushToServiceParams; - const mapping = config.incidentConfiguration - ? buildMap(config.incidentConfiguration.mapping) - : null; - const externalObject = - config.incidentConfiguration && mapping - ? mapParams(restParams as ExecutorSubActionPushParams, mapping) - : {}; - data = await api.pushToService({ externalService, - mapping, - params: { ...pushToServiceParams, externalObject }, + params: pushToServiceParams, logger, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts index 2b2a22a66b709..e1447e7718fb7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts @@ -6,8 +6,6 @@ import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types'; -import { MapRecord } from '../case/types'; - export const resilientFields = [ { id: 17, @@ -348,62 +346,28 @@ const externalServiceMock = { create: createMock, }; -const mapping: Map> = new Map(); - -mapping.set('title', { - target: 'name', - actionType: 'overwrite', -}); - -mapping.set('description', { - target: 'description', - actionType: 'overwrite', -}); - -mapping.set('comments', { - target: 'comments', - actionType: 'append', -}); - -mapping.set('name', { - target: 'title', - actionType: 'overwrite', -}); - const executorParams: ExecutorSubActionPushParams = { - savedObjectId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', - externalId: 'incident-3', - createdAt: '2020-06-03T15:09:13.606Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-06-03T15:09:13.606Z', - updatedBy: { fullName: 'Elastic User', username: 'elastic' }, - title: 'Incident title', - description: 'Incident description', - incidentTypes: [1001], - severityCode: 6, + incident: { + externalId: 'incident-3', + name: 'Incident title', + description: 'Incident description', + incidentTypes: [1001], + severityCode: 6, + }, comments: [ { commentId: 'case-comment-1', comment: 'A comment', - createdAt: '2020-06-03T15:09:13.606Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-06-03T15:09:13.606Z', - updatedBy: { fullName: 'Elastic User', username: 'elastic' }, }, { commentId: 'case-comment-2', comment: 'Another comment', - createdAt: '2020-06-03T15:09:13.606Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-06-03T15:09:13.606Z', - updatedBy: { fullName: 'Elastic User', username: 'elastic' }, }, ], }; const apiParams: PushToServiceApiParams = { ...executorParams, - externalObject: { name: 'Incident title', description: 'Incident description' }, }; const incidentTypes = [ @@ -457,4 +421,4 @@ const severity = [ }, ]; -export { externalServiceMock, mapping, executorParams, apiParams, incidentTypes, severity }; +export { externalServiceMock, executorParams, apiParams, incidentTypes, severity }; 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 c7ceba94140fb..06bfeade2c7d2 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 @@ -5,14 +5,10 @@ */ import { schema } from '@kbn/config-schema'; -import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from '../case/schema'; export const ExternalIncidentServiceConfiguration = { apiUrl: schema.string(), orgId: schema.string(), - // TODO: to remove - set it optional for the current stage to support Case implementation - incidentConfiguration: schema.nullable(IncidentConfigurationSchema), - isCaseOwned: schema.nullable(schema.boolean()), }; export const ExternalIncidentServiceConfigurationSchema = schema.object( @@ -37,15 +33,21 @@ export const ExecutorSubActionSchema = schema.oneOf([ ]); export const ExecutorSubActionPushParamsSchema = schema.object({ - savedObjectId: schema.nullable(schema.string()), - title: schema.string(), - description: schema.nullable(schema.string()), - externalId: schema.nullable(schema.string()), - incidentTypes: schema.nullable(schema.arrayOf(schema.number())), - severityCode: schema.nullable(schema.number()), - // TODO: remove later - need for support Case push multiple comments - comments: schema.nullable(schema.arrayOf(CommentSchema)), - ...EntityInformation, + incident: schema.object({ + name: schema.string(), + description: schema.nullable(schema.string()), + externalId: schema.nullable(schema.string()), + incidentTypes: schema.nullable(schema.arrayOf(schema.number())), + severityCode: schema.nullable(schema.number()), + }), + comments: schema.nullable( + schema.arrayOf( + schema.object({ + comment: schema.string(), + commentId: schema.string(), + }) + ) + ), }); export const ExecutorSubActionGetIncidentParamsSchema = schema.object({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts index 9362b0d4d2bad..97d8b64fb6535 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts @@ -450,10 +450,6 @@ describe('IBM Resilient service', () => { comment: { comment: 'comment', commentId: 'comment-1', - createdBy: null, - createdAt: null, - updatedAt: null, - updatedBy: null, }, }); @@ -477,10 +473,6 @@ describe('IBM Resilient service', () => { comment: { comment: 'comment', commentId: 'comment-1', - createdBy: null, - createdAt: null, - updatedAt: null, - updatedBy: null, }, }); @@ -510,10 +502,6 @@ describe('IBM Resilient service', () => { comment: { comment: 'comment', commentId: 'comment-1', - createdBy: null, - createdAt: null, - updatedAt: null, - updatedBy: null, }, }) ).rejects.toThrow( diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/translations.ts index 8c6ce9902da81..72bfc8001532e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/translations.ts @@ -17,11 +17,3 @@ export const ALLOWED_HOSTS_ERROR = (message: string) => message, }, }); - -// TODO: remove when Case mappings will be removed -export const MAPPING_EMPTY = i18n.translate( - 'xpack.actions.builtin.servicenow.configuration.emptyMapping', - { - defaultMessage: '[incidentConfiguration.mapping]: expected non-empty but got empty', - } -); diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts index a70420b30a092..ad6ae4b1d8386 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts @@ -22,9 +22,6 @@ import { import { ActionsConfigurationUtilities } from '../../actions_config'; import { Logger } from '../../../../../../src/core/server'; -import { IncidentConfigurationSchema } from '../case/schema'; -import { Comment } from '../case/types'; - export type ResilientPublicConfigurationType = TypeOf< typeof ExternalIncidentServiceConfigurationSchema >; @@ -39,8 +36,6 @@ export type ExecutorSubActionCommonFieldsParams = TypeOf< export type ExecutorParams = TypeOf; export type ExecutorSubActionPushParams = TypeOf; -export type IncidentConfiguration = TypeOf; - export interface ExternalServiceCredentials { config: Record; secrets: Record; @@ -58,28 +53,17 @@ export interface ExternalServiceIncidentResponse { pushedDate: string; } -export interface ExternalServiceCommentResponse { - commentId: string; - pushedDate: string; - externalCommentId?: string; -} - export type ExternalServiceParams = Record; export interface ExternalServiceFields { - id: string; input_type: string; name: string; read_only: boolean; required?: string; + text: string; } export type GetCommonFieldsResponse = ExternalServiceFields[]; -export type Incident = Pick< - ExecutorSubActionPushParams, - 'description' | 'incidentTypes' | 'severityCode' -> & { - name: string; -}; +export type Incident = Omit; export interface CreateIncidentParams { incident: Incident; @@ -92,7 +76,7 @@ export interface UpdateIncidentParams { export interface CreateCommentParams { incidentId: string; - comment: Comment; + comment: SimpleComment; } export type GetIncidentTypesResponse = Array<{ id: string; name: string }>; @@ -108,10 +92,7 @@ export interface ExternalService { updateIncident: (params: UpdateIncidentParams) => Promise; } -export interface PushToServiceApiParams extends ExecutorSubActionPushParams { - externalObject: Record; -} - +export type PushToServiceApiParams = ExecutorSubActionPushParams; export type ExecutorSubActionGetIncidentTypesParams = TypeOf< typeof ExecutorSubActionGetIncidentTypesParamsSchema >; @@ -122,7 +103,6 @@ export type ExecutorSubActionGetSeverityParams = TypeOf< export interface ExternalServiceApiHandlerArgs { externalService: ExternalService; - mapping: Map | null; } export type ExecutorSubActionGetIncidentParams = TypeOf< @@ -222,3 +202,12 @@ export interface CreateIncidentData { incident_type_ids?: Array<{ id: number }>; severity_code?: { id: number }; } +export interface SimpleComment { + comment: string; + commentId: string; +} +export interface ExternalServiceCommentResponse { + commentId: string; + pushedDate: string; + externalCommentId?: string; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/validators.ts index a50e868cdda3d..2acd558e260aa 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/validators.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty } from 'lodash'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { ResilientPublicConfigurationType, @@ -18,13 +17,6 @@ export const validateCommonConfig = ( configurationUtilities: ActionsConfigurationUtilities, configObject: ResilientPublicConfigurationType ) => { - if ( - configObject.incidentConfiguration !== null && - isEmpty(configObject.incidentConfiguration.mapping) - ) { - return i18n.MAPPING_EMPTY; - } - try { configurationUtilities.ensureUriAllowed(configObject.apiUrl); } catch (allowedListError) { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts index 4683b661e21da..772cd16cc4d51 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts @@ -5,7 +5,7 @@ */ import { Logger } from '../../../../../../src/core/server'; -import { externalServiceMock, mapping, apiParams, serviceNowCommonFields } from './mocks'; +import { externalServiceMock, apiParams, serviceNowCommonFields } from './mocks'; import { ExternalService } from './types'; import { api } from './api'; let mockedLogger: jest.Mocked; @@ -19,10 +19,9 @@ describe('api', () => { describe('create incident', () => { test('it creates an incident', async () => { - const params = { ...apiParams, externalId: null }; + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; const res = await api.pushToService({ externalService, - mapping, params, secrets: {}, logger: mockedLogger, @@ -47,10 +46,13 @@ describe('api', () => { }); test('it creates an incident without comments', async () => { - const params = { ...apiParams, externalId: null, comments: [] }; + const params = { + ...apiParams, + incident: { ...apiParams.incident, externalId: null }, + comments: [], + }; const res = await api.pushToService({ externalService, - mapping, params, secrets: {}, logger: mockedLogger, @@ -65,10 +67,12 @@ describe('api', () => { }); test('it calls createIncident correctly', async () => { - const params = { ...apiParams, externalId: null, comments: [] }; + const params = { + incident: { ...apiParams.incident, externalId: null }, + comments: [], + }; await api.pushToService({ externalService, - mapping, params, secrets: { username: 'elastic', password: 'elastic' }, logger: mockedLogger, @@ -80,18 +84,17 @@ describe('api', () => { urgency: '2', impact: '3', caller_id: 'elastic', - description: 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - short_description: 'Incident title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'Incident description', + short_description: 'Incident title', }, }); expect(externalService.updateIncident).not.toHaveBeenCalled(); }); test('it calls updateIncident correctly when creating an incident and having comments', async () => { - const params = { ...apiParams, externalId: null }; + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; await api.pushToService({ externalService, - mapping, params, secrets: {}, logger: mockedLogger, @@ -102,9 +105,9 @@ describe('api', () => { severity: '1', urgency: '2', impact: '3', - comments: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - short_description: 'Incident title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + comments: 'A comment', + description: 'Incident description', + short_description: 'Incident title', }, incidentId: 'incident-1', }); @@ -114,9 +117,9 @@ describe('api', () => { severity: '1', urgency: '2', impact: '3', - comments: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - short_description: 'Incident title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + comments: 'Another comment', + description: 'Incident description', + short_description: 'Incident title', }, incidentId: 'incident-1', }); @@ -127,7 +130,6 @@ describe('api', () => { test('it updates an incident', async () => { const res = await api.pushToService({ externalService, - mapping, params: apiParams, secrets: {}, logger: mockedLogger, @@ -155,7 +157,6 @@ describe('api', () => { const params = { ...apiParams, comments: [] }; const res = await api.pushToService({ externalService, - mapping, params, secrets: {}, logger: mockedLogger, @@ -173,7 +174,6 @@ describe('api', () => { const params = { ...apiParams }; await api.pushToService({ externalService, - mapping, params, secrets: {}, logger: mockedLogger, @@ -185,8 +185,8 @@ describe('api', () => { severity: '1', urgency: '2', impact: '3', - description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - short_description: 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'Incident description', + short_description: 'Incident title', }, }); expect(externalService.createIncident).not.toHaveBeenCalled(); @@ -196,7 +196,6 @@ describe('api', () => { const params = { ...apiParams }; await api.pushToService({ externalService, - mapping, params, secrets: {}, logger: mockedLogger, @@ -207,8 +206,8 @@ describe('api', () => { severity: '1', urgency: '2', impact: '3', - description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - short_description: 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'Incident description', + short_description: 'Incident title', }, incidentId: 'incident-3', }); @@ -218,409 +217,15 @@ describe('api', () => { severity: '1', urgency: '2', impact: '3', - comments: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - short_description: 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + comments: 'A comment', + description: 'Incident description', + short_description: 'Incident title', }, incidentId: 'incident-2', }); }); }); - describe('mapping variations', () => { - test('overwrite & append', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'append', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - severity: '1', - urgency: '2', - impact: '3', - short_description: 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: - 'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - }); - }); - - test('nothing & append', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'nothing', - }); - - mapping.set('description', { - target: 'description', - actionType: 'append', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'nothing', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - severity: '1', - urgency: '2', - impact: '3', - description: - 'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - }); - }); - - test('append & append', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'append', - }); - - mapping.set('description', { - target: 'description', - actionType: 'append', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'append', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - severity: '1', - urgency: '2', - impact: '3', - short_description: - 'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: - 'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - }); - }); - - test('nothing & nothing', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'nothing', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'nothing', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - severity: '1', - urgency: '2', - impact: '3', - }, - }); - }); - - test('overwrite & nothing', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - severity: '1', - urgency: '2', - impact: '3', - short_description: 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - }); - }); - - test('overwrite & overwrite', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - severity: '1', - urgency: '2', - impact: '3', - short_description: 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - }); - }); - - test('nothing & overwrite', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'nothing', - }); - - mapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'nothing', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - severity: '1', - urgency: '2', - impact: '3', - description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - }); - }); - - test('append & overwrite', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'append', - }); - - mapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'append', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - severity: '1', - urgency: '2', - impact: '3', - short_description: - 'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - }); - }); - - test('append & nothing', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'append', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'append', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledWith({ - incidentId: 'incident-3', - incident: { - severity: '1', - urgency: '2', - impact: '3', - short_description: - 'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - }); - }); - - test('comment nothing', async () => { - mapping.set('title', { - target: 'short_description', - actionType: 'overwrite', - }); - - mapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - mapping.set('comments', { - target: 'comments', - actionType: 'nothing', - }); - - mapping.set('short_description', { - target: 'title', - actionType: 'overwrite', - }); - - await api.pushToService({ - externalService, - mapping, - params: apiParams, - secrets: {}, - logger: mockedLogger, - }); - expect(externalService.updateIncident).toHaveBeenCalledTimes(1); - }); - }); - describe('getFields', () => { test('it returns the fields correctly', async () => { const res = await api.getFields({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts index fbd8fdd635d70..9981a8431a736 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts @@ -4,86 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ import { - ExternalServiceParams, - PushToServiceApiHandlerArgs, - HandshakeApiHandlerArgs, - GetIncidentApiHandlerArgs, ExternalServiceApi, - PushToServiceApiParams, - PushToServiceResponse, - Incident, GetCommonFieldsHandlerArgs, GetCommonFieldsResponse, + GetIncidentApiHandlerArgs, + HandshakeApiHandlerArgs, + Incident, + PushToServiceApiHandlerArgs, + PushToServiceResponse, } from './types'; -// TODO: to remove, need to support Case -import { transformFields, transformComments, prepareFieldsForTransformation } from '../case/utils'; - -const handshakeHandler = async ({ - externalService, - mapping, - params, -}: HandshakeApiHandlerArgs) => {}; -const getIncidentHandler = async ({ - externalService, - mapping, - params, -}: GetIncidentApiHandlerArgs) => {}; +const handshakeHandler = async ({ externalService, params }: HandshakeApiHandlerArgs) => {}; +const getIncidentHandler = async ({ externalService, params }: GetIncidentApiHandlerArgs) => {}; const pushToServiceHandler = async ({ externalService, - mapping, params, secrets, - logger, }: PushToServiceApiHandlerArgs): Promise => { - const { externalId, comments } = params; - const updateIncident = externalId ? true : false; - const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated']; - let currentIncident: ExternalServiceParams | undefined; + const { comments } = params; let res: PushToServiceResponse; + const { externalId, ...rest } = params.incident; + const incident: Incident = rest; - if (externalId) { - try { - currentIncident = await externalService.getIncident(externalId); - } catch (ex) { - logger.debug( - `Retrieving Incident by id ${externalId} from ServiceNow was failed with exception: ${ex}` - ); - } - } - - let incident = {}; - // TODO: should be removed later but currently keep it for the Case implementation support - if (mapping && Array.isArray(params.comments)) { - const fields = prepareFieldsForTransformation({ - externalCase: params.externalObject, - mapping, - defaultPipes, - }); - - const transformedFields = transformFields< - PushToServiceApiParams, - ExternalServiceParams, - Incident - >({ - params, - fields, - currentIncident, - }); - - incident = { - severity: params.severity, - urgency: params.urgency, - impact: params.impact, - short_description: transformedFields.short_description, - description: transformedFields.description, - }; - } else { - incident = { ...params, short_description: params.title, comments: params.comment }; - } - - if (updateIncident) { + if (externalId != null) { res = await externalService.updateIncident({ incidentId: externalId, incident, @@ -97,24 +41,15 @@ const pushToServiceHandler = async ({ }); } - // TODO: should temporary keep comments for a Case usage - if ( - comments && - Array.isArray(comments) && - comments.length > 0 && - mapping && - mapping.get('comments')?.actionType !== 'nothing' - ) { + if (comments && Array.isArray(comments) && comments.length > 0) { res.comments = []; - const commentsTransformed = transformComments(comments, ['informationAdded']); - const fieldsKey = mapping.get('comments')?.target ?? 'comments'; - for (const currentComment of commentsTransformed) { + for (const currentComment of comments) { await externalService.updateIncident({ incidentId: res.id, incident: { ...incident, - [fieldsKey]: currentComment.comment, + comments: currentComment.comment, }, }); res.comments = [ diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index d1182b0d3b2fa..3fa8b25b86e8b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -29,9 +29,6 @@ import { ServiceNowExecutorResultData, } from './types'; -// TODO: to remove, need to support Case -import { buildMap, mapParams } from '../case/utils'; - interface GetActionTypeParams { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; @@ -101,17 +98,9 @@ async function executor( if (subAction === 'pushToService') { const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; - - const { comments, externalId, ...restParams } = pushToServiceParams; - const incidentConfiguration = config.incidentConfiguration; - const mapping = incidentConfiguration ? buildMap(incidentConfiguration.mapping) : null; - const externalObject = - config.incidentConfiguration && mapping ? mapParams(restParams, mapping) : {}; - data = await api.pushToService({ externalService, - mapping, - params: { ...pushToServiceParams, externalObject }, + params: pushToServiceParams, secrets, logger, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts index 2351be36a50c4..9d9b1e164e7dd 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts @@ -5,7 +5,6 @@ */ import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types'; -import { MapRecord } from '../case/types'; export const serviceNowCommonFields = [ { @@ -69,64 +68,29 @@ const externalServiceMock = { create: createMock, }; -const mapping: Map> = new Map(); - -mapping.set('title', { - target: 'short_description', - actionType: 'overwrite', -}); - -mapping.set('description', { - target: 'description', - actionType: 'overwrite', -}); - -mapping.set('comments', { - target: 'comments', - actionType: 'append', -}); - -mapping.set('short_description', { - target: 'title', - actionType: 'overwrite', -}); - const executorParams: ExecutorSubActionPushParams = { - savedObjectId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', - externalId: 'incident-3', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { fullName: 'Elastic User', username: 'elastic' }, - title: 'Incident title', - description: 'Incident description', - comment: 'test-alert comment', - severity: '1', - urgency: '2', - impact: '3', + incident: { + externalId: 'incident-3', + short_description: 'Incident title', + description: 'Incident description', + severity: '1', + urgency: '2', + impact: '3', + }, comments: [ { commentId: 'case-comment-1', comment: 'A comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { fullName: 'Elastic User', username: 'elastic' }, }, { commentId: 'case-comment-2', comment: 'Another comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { fullName: 'Elastic User', username: 'elastic' }, }, ], }; const apiParams: PushToServiceApiParams = { ...executorParams, - externalObject: { short_description: 'Incident title', description: 'Incident description' }, }; -export { externalServiceMock, mapping, executorParams, apiParams }; +export { externalServiceMock, executorParams, apiParams }; 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 77c48aab1f309..1c05fa93f2362 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 @@ -5,13 +5,9 @@ */ import { schema } from '@kbn/config-schema'; -import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from '../case/schema'; export const ExternalIncidentServiceConfiguration = { apiUrl: schema.string(), - // TODO: to remove - set it optional for the current stage to support Case ServiceNow implementation - incidentConfiguration: schema.nullable(IncidentConfigurationSchema), - isCaseOwned: schema.maybe(schema.boolean()), }; export const ExternalIncidentServiceConfigurationSchema = schema.object( @@ -35,17 +31,22 @@ export const ExecutorSubActionSchema = schema.oneOf([ ]); export const ExecutorSubActionPushParamsSchema = schema.object({ - savedObjectId: schema.nullable(schema.string()), - title: schema.string(), - description: schema.nullable(schema.string()), - comment: schema.nullable(schema.string()), - externalId: schema.nullable(schema.string()), - severity: schema.nullable(schema.string()), - urgency: schema.nullable(schema.string()), - impact: schema.nullable(schema.string()), - // TODO: remove later - need for support Case push multiple comments - comments: schema.maybe(schema.arrayOf(CommentSchema)), - ...EntityInformation, + incident: schema.object({ + short_description: schema.string(), + description: schema.nullable(schema.string()), + externalId: schema.nullable(schema.string()), + severity: schema.nullable(schema.string()), + urgency: schema.nullable(schema.string()), + impact: schema.nullable(schema.string()), + }), + comments: schema.nullable( + schema.arrayOf( + schema.object({ + comment: schema.string(), + commentId: schema.string(), + }) + ) + ), }); export const ExecutorSubActionGetIncidentParamsSchema = schema.object({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts index 3e4873270ad7a..1a6412f9ceb5b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts @@ -249,7 +249,7 @@ describe('ServiceNow service', () => { axios, logger, url: - 'https://dev102283.service-now.com/api/now/v2/table/sys_dictionary?sysparm_query=name=task^internal_type=string&active=true&read_only=false&sysparm_fields=max_length,element,column_label', + 'https://dev102283.service-now.com/api/now/v2/table/sys_dictionary?sysparm_query=name=task^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory', }); }); test('it returns common fields correctly', async () => { @@ -265,7 +265,7 @@ describe('ServiceNow service', () => { throw new Error('An error has occurred'); }); await expect(service.getFields()).rejects.toThrow( - 'Unable to get common fields. Error: An error has occurred' + '[Action][ServiceNow]: Unable to get fields. Error: An error has occurred' ); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts index 29614a4b951e1..96faf6d338b90 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import axios from 'axios'; +import axios, { AxiosResponse } from 'axios'; import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from './types'; @@ -35,7 +35,7 @@ export const createExternalService = ( const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url; const incidentUrl = `${urlWithoutTrailingSlash}/${INCIDENT_URL}`; - const fieldsUrl = `${urlWithoutTrailingSlash}/${SYS_DICTIONARY}?sysparm_query=name=task^internal_type=string&active=true&read_only=false&sysparm_fields=max_length,element,column_label`; + const fieldsUrl = `${urlWithoutTrailingSlash}/${SYS_DICTIONARY}?sysparm_query=name=task^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory`; const axiosInstance = axios.create({ auth: { username, password }, }); @@ -44,6 +44,14 @@ export const createExternalService = ( return `${urlWithoutTrailingSlash}/${VIEW_INCIDENT_URL}${id}`; }; + const checkInstance = (res: AxiosResponse) => { + if (res.status === 200 && res.data.result == null) { + throw new Error( + `There is an issue with your Service Now Instance. Please check ${res.request.connection.servername}` + ); + } + }; + const getIncident = async (id: string) => { try { const res = await request({ @@ -52,7 +60,7 @@ export const createExternalService = ( logger, proxySettings, }); - + checkInstance(res); return { ...res.data.result }; } catch (error) { throw new Error( @@ -70,7 +78,7 @@ export const createExternalService = ( proxySettings, params, }); - + checkInstance(res); return res.data.result.length > 0 ? { ...res.data.result } : undefined; } catch (error) { throw new Error( @@ -89,7 +97,7 @@ export const createExternalService = ( method: 'post', data: { ...(incident as Record) }, }); - + checkInstance(res); return { title: res.data.result.number, id: res.data.result.sys_id, @@ -112,7 +120,7 @@ export const createExternalService = ( data: { ...(incident as Record) }, proxySettings, }); - + checkInstance(res); return { title: res.data.result.number, id: res.data.result.sys_id, @@ -137,12 +145,10 @@ export const createExternalService = ( logger, proxySettings, }); - + checkInstance(res); return res.data.result.length > 0 ? res.data.result : []; } catch (error) { - throw new Error( - getErrorMessage(i18n.NAME, `Unable to get common fields. Error: ${error.message}`) - ); + throw new Error(getErrorMessage(i18n.NAME, `Unable to get fields. Error: ${error.message}`)); } }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts index 7cc97a241c4bc..287fe8cacda79 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts @@ -17,11 +17,3 @@ export const ALLOWED_HOSTS_ERROR = (message: string) => message, }, }); - -// TODO: remove when Case mappings will be removed -export const MAPPING_EMPTY = i18n.translate( - 'xpack.actions.builtin.servicenow.configuration.emptyMapping', - { - defaultMessage: '[incidentConfiguration.mapping]: expected non-empty but got empty', - } -); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index 0ee03f883ec05..9868f5d1bea06 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -17,8 +17,6 @@ import { ExternalIncidentServiceSecretConfigurationSchema, } from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; -import { ExternalServiceCommentResponse } from '../case/types'; -import { IncidentConfigurationSchema } from '../case/schema'; import { Logger } from '../../../../../../src/core/server'; export type ServiceNowPublicConfigurationType = TypeOf< @@ -41,8 +39,6 @@ export interface CreateCommentRequest { export type ExecutorParams = TypeOf; export type ExecutorSubActionPushParams = TypeOf; -export type IncidentConfiguration = TypeOf; - export interface ExternalServiceCredentials { config: Record; secrets: Record; @@ -73,13 +69,10 @@ export interface ExternalService { findIncidents: (params?: Record) => Promise; } -export interface PushToServiceApiParams extends ExecutorSubActionPushParams { - externalObject: Record; -} +export type PushToServiceApiParams = ExecutorSubActionPushParams; export interface ExternalServiceApiHandlerArgs { externalService: ExternalService; - mapping: Map | null; } export type ExecutorSubActionGetIncidentParams = TypeOf< @@ -90,12 +83,7 @@ export type ExecutorSubActionHandshakeParams = TypeOf< typeof ExecutorSubActionHandshakeParamsSchema >; -export type Incident = Pick< - ExecutorSubActionPushParams, - 'description' | 'severity' | 'urgency' | 'impact' -> & { - short_description: string; -}; +export type Incident = Omit; export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { params: PushToServiceApiParams; @@ -112,11 +100,7 @@ export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs { } export interface ExternalServiceFields { column_label: string; - name: string; - internal_type: { - link: string; - value: string; - }; + mandatory: string; max_length: string; element: string; } @@ -132,3 +116,9 @@ export interface ExternalServiceApi { pushToService: (args: PushToServiceApiHandlerArgs) => Promise; getIncident: (args: GetIncidentApiHandlerArgs) => Promise; } + +export interface ExternalServiceCommentResponse { + commentId: string; + pushedDate: string; + externalCommentId?: string; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts index 87bbfd9c7ea95..07c6d83a13042 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty } from 'lodash'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { ServiceNowPublicConfigurationType, @@ -18,13 +17,6 @@ export const validateCommonConfig = ( configurationUtilities: ActionsConfigurationUtilities, configObject: ServiceNowPublicConfigurationType ) => { - if ( - configObject.incidentConfiguration !== null && - isEmpty(configObject.incidentConfiguration.mapping) - ) { - return i18n.MAPPING_EMPTY; - } - try { configurationUtilities.ensureUriAllowed(configObject.apiUrl); } catch (allowedListError) { diff --git a/x-pack/plugins/actions/server/saved_objects/migrations.test.ts b/x-pack/plugins/actions/server/saved_objects/migrations.test.ts index f1bd1ba2aeb60..fc0d9a45282ce 100644 --- a/x-pack/plugins/actions/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/actions/server/saved_objects/migrations.test.ts @@ -93,6 +93,21 @@ describe('7.11.0', () => { }, }); }); + test('remove cases mapping object', () => { + const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0']; + const action = getMockData({ + config: { incidentConfiguration: { mapping: [] }, isCaseOwned: true, another: 'value' }, + }); + expect(migration711(action, context)).toEqual({ + ...action, + attributes: { + ...action.attributes, + config: { + another: 'value', + }, + }, + }); + }); }); function getMockDataForWebhook( diff --git a/x-pack/plugins/actions/server/saved_objects/migrations.ts b/x-pack/plugins/actions/server/saved_objects/migrations.ts index 1e2290b14ec1b..1045047d97186 100644 --- a/x-pack/plugins/actions/server/saved_objects/migrations.ts +++ b/x-pack/plugins/actions/server/saved_objects/migrations.ts @@ -19,24 +19,23 @@ type ActionMigration = ( export function getMigrations( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup ): SavedObjectMigrationMap { - const migrationActions = encryptedSavedObjects.createMigration( + const migrationActionsTen = encryptedSavedObjects.createMigration( (doc): doc is SavedObjectUnsanitizedDoc => !!doc.attributes.config?.casesConfiguration || doc.attributes.actionTypeId === '.email', pipeMigrations(renameCasesConfigurationObject, addHasAuthConfigurationObject) ); - const migrationWebhookConnectorHasAuth = encryptedSavedObjects.createMigration< - RawAction, - RawAction - >( + const migrationActionsEleven = encryptedSavedObjects.createMigration( (doc): doc is SavedObjectUnsanitizedDoc => + !!doc.attributes.config?.isCaseOwned || + !!doc.attributes.config?.incidentConfiguration || doc.attributes.actionTypeId === '.webhook', - pipeMigrations(addHasAuthConfigurationObject) + pipeMigrations(removeCasesFieldMappings, addHasAuthConfigurationObject) ); return { - '7.10.0': executeMigrationWithErrorHandling(migrationActions, '7.10.0'), - '7.11.0': executeMigrationWithErrorHandling(migrationWebhookConnectorHasAuth, '7.11.0'), + '7.10.0': executeMigrationWithErrorHandling(migrationActionsTen, '7.10.0'), + '7.11.0': executeMigrationWithErrorHandling(migrationActionsEleven, '7.11.0'), }; } @@ -77,6 +76,26 @@ function renameCasesConfigurationObject( }; } +function removeCasesFieldMappings( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + if ( + !doc.attributes.config?.hasOwnProperty('isCaseOwned') && + !doc.attributes.config?.hasOwnProperty('incidentConfiguration') + ) { + return doc; + } + const { incidentConfiguration, isCaseOwned, ...restConfiguration } = doc.attributes.config; + + return { + ...doc, + attributes: { + ...doc.attributes, + config: restConfiguration, + }, + }; +} + const addHasAuthConfigurationObject = ( doc: SavedObjectUnsanitizedDoc ): SavedObjectUnsanitizedDoc => { diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index f55b088c4d3f6..b311a602212c7 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -18,6 +18,9 @@ import { } from '../../../../src/core/server'; import { ActionTypeExecutorResult } from '../common'; 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 type WithoutQueryAndParams = Pick>; export type GetServicesFunction = (request: KibanaRequest) => Services; diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts index a08e1fbca66ea..9b6945edfe729 100644 --- a/x-pack/plugins/case/common/api/cases/case.ts +++ b/x-pack/plugins/case/common/api/cases/case.ts @@ -10,10 +10,7 @@ import { NumberFromString } from '../saved_object'; import { UserRT } from '../user'; import { CommentResponseRt } from './comment'; import { CasesStatusResponseRt } from './status'; -import { CaseConnectorRt, ESCaseConnector, ConnectorPartialFieldsRt } from '../connectors'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { ActionTypeExecutorResult } from '../../../../actions/server/types'; +import { CaseConnectorRt, ESCaseConnector } from '../connectors'; export enum CaseStatuses { open = 'open', @@ -128,66 +125,6 @@ export const CasePatchRequestRt = rt.intersection([ export const CasesPatchRequestRt = rt.type({ cases: rt.array(CasePatchRequestRt) }); export const CasesResponseRt = rt.array(CaseResponseRt); -/* - * This type are related to this file below - * x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts - * why because this schema is not share in a common folder - * so we redefine then so we can use/validate types - */ - -// TODO: Refactor to support multiple connectors with various fields - -const ServiceConnectorUserParams = rt.type({ - fullName: rt.union([rt.string, rt.null]), - username: rt.string, -}); - -export const ServiceConnectorCommentParamsRt = rt.type({ - commentId: rt.string, - comment: rt.string, - createdAt: rt.string, - createdBy: ServiceConnectorUserParams, - updatedAt: rt.union([rt.string, rt.null]), - updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), -}); - -export const ServiceConnectorBasicCaseParamsRt = rt.type({ - comments: rt.union([rt.array(ServiceConnectorCommentParamsRt), rt.null]), - createdAt: rt.string, - createdBy: ServiceConnectorUserParams, - description: rt.union([rt.string, rt.null]), - externalId: rt.union([rt.string, rt.null]), - savedObjectId: rt.string, - title: rt.string, - updatedAt: rt.union([rt.string, rt.null]), - updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), -}); - -export const ServiceConnectorCaseParamsRt = rt.intersection([ - ServiceConnectorBasicCaseParamsRt, - ConnectorPartialFieldsRt, -]); - -export const ServiceConnectorCaseResponseRt = rt.intersection([ - rt.type({ - title: rt.string, - id: rt.string, - pushedDate: rt.string, - url: rt.string, - }), - rt.partial({ - comments: rt.array( - rt.intersection([ - rt.type({ - commentId: rt.string, - pushedDate: rt.string, - }), - rt.partial({ externalCommentId: rt.string }), - ]) - ), - }), -]); - export type CaseAttributes = rt.TypeOf; export type CasePostRequest = rt.TypeOf; export type CaseResponse = rt.TypeOf; @@ -196,10 +133,7 @@ export type CasesFindResponse = rt.TypeOf; export type CasePatchRequest = rt.TypeOf; export type CasesPatchRequest = rt.TypeOf; export type CaseExternalServiceRequest = rt.TypeOf; -export type ServiceConnectorCaseParams = rt.TypeOf; -export type ServiceConnectorCaseResponse = rt.TypeOf; export type CaseFullExternalService = rt.TypeOf; -export type ServiceConnectorCommentParams = rt.TypeOf; export type ESCaseAttributes = Omit & { connector: ESCaseConnector }; export type ESCasePatchRequest = Omit & { diff --git a/x-pack/plugins/case/common/api/cases/configure.ts b/x-pack/plugins/case/common/api/cases/configure.ts index b0a2fa6576fd7..84f0e1fea6edf 100644 --- a/x-pack/plugins/case/common/api/cases/configure.ts +++ b/x-pack/plugins/case/common/api/cases/configure.ts @@ -8,60 +8,7 @@ import * as rt from 'io-ts'; import { ActionResult } from '../../../../actions/common'; import { UserRT } from '../user'; -import { JiraCaseFieldsRt } from '../connectors/jira'; -import { ServiceNowCaseFieldsRT } from '../connectors/servicenow'; -import { ResilientCaseFieldsRT } from '../connectors/resilient'; -import { CaseConnectorRt, ESCaseConnector } from '../connectors'; - -/* - * This types below are related to the service now configuration - * mapping between our case and [service-now, jira] - * - */ - -const ActionTypeRT = rt.union([ - rt.literal('append'), - rt.literal('nothing'), - rt.literal('overwrite'), -]); - -const CaseFieldRT = rt.union([ - rt.literal('title'), - rt.literal('description'), - rt.literal('comments'), -]); - -const ThirdPartyFieldRT = rt.union([ - JiraCaseFieldsRt, - ServiceNowCaseFieldsRT, - ResilientCaseFieldsRT, - rt.literal('not_mapped'), -]); - -export const CasesConfigurationMapsRT = rt.type({ - source: CaseFieldRT, - target: ThirdPartyFieldRT, - action_type: ActionTypeRT, -}); - -export const CasesConfigurationRT = rt.type({ - mapping: rt.array(CasesConfigurationMapsRT), -}); - -export const CasesConnectorConfigurationRT = rt.type({ - cases_configuration: CasesConfigurationRT, - // version: rt.string, -}); - -export type ActionType = rt.TypeOf; -export type CaseField = rt.TypeOf; -export type ThirdPartyField = rt.TypeOf; - -export type CasesConfigurationMaps = rt.TypeOf; -export type CasesConfiguration = rt.TypeOf; -export type CasesConnectorConfiguration = rt.TypeOf; - -/** ********************************************************************** */ +import { CaseConnectorRt, ConnectorMappingsRt, ESCaseConnector } from '../connectors'; export type ActionConnector = ActionResult; @@ -91,6 +38,7 @@ export const CaseConfigureAttributesRt = rt.intersection([ export const CaseConfigureResponseRt = rt.intersection([ CaseConfigureAttributesRt, + ConnectorMappingsRt, rt.type({ version: rt.string, }), diff --git a/x-pack/plugins/case/common/api/connectors/index.ts b/x-pack/plugins/case/common/api/connectors/index.ts index 0019afe7c6b74..f44e0f326a829 100644 --- a/x-pack/plugins/case/common/api/connectors/index.ts +++ b/x-pack/plugins/case/common/api/connectors/index.ts @@ -12,6 +12,7 @@ import { ServiceNowFieldsRT } from './servicenow'; export * from './jira'; export * from './servicenow'; export * from './resilient'; +export * from './mappings'; export const ConnectorFieldsRt = rt.union([ JiraFieldsRT, @@ -19,13 +20,6 @@ export const ConnectorFieldsRt = rt.union([ ServiceNowFieldsRT, rt.null, ]); - -export const ConnectorPartialFieldsRt = rt.partial({ - ...JiraFieldsRT.props, - ...ResilientFieldsRT.props, - ...ServiceNowFieldsRT.props, -}); - export enum ConnectorTypes { jira = '.jira', resilient = '.resilient', diff --git a/x-pack/plugins/case/common/api/connectors/jira.ts b/x-pack/plugins/case/common/api/connectors/jira.ts index f6a45d9872fcc..3ff6857a4fb97 100644 --- a/x-pack/plugins/case/common/api/connectors/jira.ts +++ b/x-pack/plugins/case/common/api/connectors/jira.ts @@ -6,12 +6,6 @@ import * as rt from 'io-ts'; -export const JiraCaseFieldsRt = rt.union([ - rt.literal('summary'), - rt.literal('description'), - rt.literal('comments'), -]); - export const JiraFieldsRT = rt.type({ issueType: rt.union([rt.string, rt.null]), priority: rt.union([rt.string, rt.null]), diff --git a/x-pack/plugins/case/common/api/connectors/mappings.ts b/x-pack/plugins/case/common/api/connectors/mappings.ts new file mode 100644 index 0000000000000..3e8baf0af2834 --- /dev/null +++ b/x-pack/plugins/case/common/api/connectors/mappings.ts @@ -0,0 +1,191 @@ +/* + * 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. + */ + +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import * as rt from 'io-ts'; +import { ElasticUser } from '../../../../security_solution/public/cases/containers/types'; +import { + PushToServiceApiParams as JiraPushToServiceApiParams, + Incident as JiraIncident, +} from '../../../../actions/server/builtin_action_types/jira/types'; +import { + PushToServiceApiParams as ResilientPushToServiceApiParams, + Incident as ResilientIncident, +} from '../../../../actions/server/builtin_action_types/resilient/types'; +import { + PushToServiceApiParams as ServiceNowPushToServiceApiParams, + Incident as ServiceNowIncident, +} from '../../../../actions/server/builtin_action_types/servicenow/types'; +import { ResilientFieldsRT } from './resilient'; +import { ServiceNowFieldsRT } from './servicenow'; +import { JiraFieldsRT } from './jira'; + +export { + JiraPushToServiceApiParams, + ResilientPushToServiceApiParams, + ServiceNowPushToServiceApiParams, +}; +export type Incident = JiraIncident | ResilientIncident | ServiceNowIncident; +export type PushToServiceApiParams = + | JiraPushToServiceApiParams + | ResilientPushToServiceApiParams + | ServiceNowPushToServiceApiParams; + +const ActionTypeRT = rt.union([ + rt.literal('append'), + rt.literal('nothing'), + rt.literal('overwrite'), +]); +const CaseFieldRT = rt.union([ + rt.literal('title'), + rt.literal('description'), + rt.literal('comments'), +]); +const ThirdPartyFieldRT = rt.union([rt.string, rt.literal('not_mapped')]); +export type ActionType = rt.TypeOf; +export type CaseField = rt.TypeOf; +export type ThirdPartyField = rt.TypeOf; + +export const ConnectorMappingsAttributesRT = rt.type({ + action_type: ActionTypeRT, + source: CaseFieldRT, + target: ThirdPartyFieldRT, +}); +export const ConnectorMappingsRt = rt.type({ + mappings: rt.array(ConnectorMappingsAttributesRT), +}); +export type ConnectorMappingsAttributes = rt.TypeOf; +export type ConnectorMappings = rt.TypeOf; + +const FieldTypeRT = rt.union([rt.literal('text'), rt.literal('textarea')]); + +const ConnectorFieldRt = rt.type({ + id: rt.string, + name: rt.string, + required: rt.boolean, + type: FieldTypeRT, +}); +export type ConnectorField = rt.TypeOf; +export const ConnectorRequestParamsRt = rt.type({ + connector_id: rt.string, +}); +export const GetFieldsRequestQueryRt = rt.type({ + connector_type: rt.string, +}); +const GetFieldsResponseRt = rt.type({ + defaultMappings: rt.array(ConnectorMappingsAttributesRT), + fields: rt.array(ConnectorFieldRt), +}); +export type GetFieldsResponse = rt.TypeOf; + +export type ExternalServiceParams = Record; + +export interface PipedField { + actionType: string; + key: string; + pipes: string[]; + value: string; +} +export interface PrepareFieldsForTransformArgs { + defaultPipes: string[]; + mappings: ConnectorMappingsAttributes[]; + params: ServiceConnectorCaseParams; +} +export interface EntityInformation { + createdAt: string; + createdBy: ElasticUser; + updatedAt: string | null; + updatedBy: ElasticUser | null; +} +export interface TransformerArgs { + date?: string; + previousValue?: string; + user?: string; + value: string; +} + +export type Transformer = (args: TransformerArgs) => TransformerArgs; +export interface TransformFieldsArgs { + currentIncident?: S; + fields: PipedField[]; + params: P; +} + +export const ServiceConnectorUserParams = rt.type({ + fullName: rt.union([rt.string, rt.null]), + username: rt.string, +}); + +export const ServiceConnectorCommentParamsRt = rt.type({ + commentId: rt.string, + comment: rt.string, + createdAt: rt.string, + createdBy: ServiceConnectorUserParams, + updatedAt: rt.union([rt.string, rt.null]), + updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), +}); +export const ServiceConnectorBasicCaseParamsRt = rt.type({ + comments: rt.union([rt.array(ServiceConnectorCommentParamsRt), rt.null]), + createdAt: rt.string, + createdBy: ServiceConnectorUserParams, + description: rt.union([rt.string, rt.null]), + externalId: rt.union([rt.string, rt.null]), + savedObjectId: rt.string, + title: rt.string, + updatedAt: rt.union([rt.string, rt.null]), + updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), +}); + +export const ConnectorPartialFieldsRt = rt.partial({ + ...JiraFieldsRT.props, + ...ResilientFieldsRT.props, + ...ServiceNowFieldsRT.props, +}); + +export const ServiceConnectorCaseParamsRt = rt.intersection([ + ServiceConnectorBasicCaseParamsRt, + ConnectorPartialFieldsRt, +]); + +export const ServiceConnectorCaseResponseRt = rt.intersection([ + rt.type({ + title: rt.string, + id: rt.string, + pushedDate: rt.string, + url: rt.string, + }), + rt.partial({ + comments: rt.array( + rt.intersection([ + rt.type({ + commentId: rt.string, + pushedDate: rt.string, + }), + rt.partial({ externalCommentId: rt.string }), + ]) + ), + }), +]); +export type ServiceConnectorBasicCaseParams = rt.TypeOf; +export type ServiceConnectorCaseParams = rt.TypeOf; +export type ServiceConnectorCaseResponse = rt.TypeOf; +export type ServiceConnectorCommentParams = rt.TypeOf; + +export const PostPushRequestRt = rt.type({ + connector_type: rt.string, + params: ServiceConnectorCaseParamsRt, +}); + +export interface SimpleComment { + comment: string; + commentId: string; +} + +export interface MapIncident { + incident: ExternalServiceParams; + comments: SimpleComment[]; +} diff --git a/x-pack/plugins/case/common/api/connectors/resilient.ts b/x-pack/plugins/case/common/api/connectors/resilient.ts index c2f7beb7626aa..5be14c1a930c3 100644 --- a/x-pack/plugins/case/common/api/connectors/resilient.ts +++ b/x-pack/plugins/case/common/api/connectors/resilient.ts @@ -6,12 +6,6 @@ import * as rt from 'io-ts'; -export const ResilientCaseFieldsRT = rt.union([ - rt.literal('name'), - rt.literal('description'), - rt.literal('comments'), -]); - export const ResilientFieldsRT = rt.type({ incidentTypes: rt.union([rt.array(rt.string), rt.null]), severityCode: rt.union([rt.string, rt.null]), diff --git a/x-pack/plugins/case/common/api/connectors/servicenow.ts b/x-pack/plugins/case/common/api/connectors/servicenow.ts index efcbeed714210..adb152981d902 100644 --- a/x-pack/plugins/case/common/api/connectors/servicenow.ts +++ b/x-pack/plugins/case/common/api/connectors/servicenow.ts @@ -6,12 +6,6 @@ import * as rt from 'io-ts'; -export const ServiceNowCaseFieldsRT = rt.union([ - rt.literal('short_description'), - rt.literal('description'), - rt.literal('comments'), -]); - export const ServiceNowFieldsRT = rt.type({ impact: rt.union([rt.string, rt.null]), severity: rt.union([rt.string, rt.null]), diff --git a/x-pack/plugins/case/common/api/helpers.ts b/x-pack/plugins/case/common/api/helpers.ts index 0efdcd3819659..3ed12ba9a68b0 100644 --- a/x-pack/plugins/case/common/api/helpers.ts +++ b/x-pack/plugins/case/common/api/helpers.ts @@ -9,6 +9,7 @@ import { CASE_COMMENTS_URL, CASE_USER_ACTIONS_URL, CASE_COMMENT_DETAILS_URL, + CASE_CONFIGURE_PUSH_URL, } from '../constants'; export const getCaseDetailsUrl = (id: string): string => { @@ -26,3 +27,6 @@ export const getCaseCommentDetailsUrl = (caseId: string, commentId: string): str export const getCaseUserActionUrl = (id: string): string => { return CASE_USER_ACTIONS_URL.replace('{case_id}', id); }; +export const getCaseConfigurePushUrl = (id: string): string => { + return CASE_CONFIGURE_PUSH_URL.replace('{connector_id}', id); +}; diff --git a/x-pack/plugins/case/common/api/saved_object.ts b/x-pack/plugins/case/common/api/saved_object.ts index 73fe767dd717e..58cbc676d2e6a 100644 --- a/x-pack/plugins/case/common/api/saved_object.ts +++ b/x-pack/plugins/case/common/api/saved_object.ts @@ -21,6 +21,7 @@ export const NumberFromString = new rt.Type( export const SavedObjectFindOptionsRt = rt.partial({ defaultSearchOperator: rt.union([rt.literal('AND'), rt.literal('OR')]), + hasReference: rt.type({ id: rt.string, type: rt.string }), fields: rt.array(rt.string), filter: rt.string, page: NumberFromString, diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts index 15a318002390f..f4823e81a468b 100644 --- a/x-pack/plugins/case/common/constants.ts +++ b/x-pack/plugins/case/common/constants.ts @@ -14,6 +14,8 @@ export const CASES_URL = '/api/cases'; export const CASE_DETAILS_URL = `${CASES_URL}/{case_id}`; export const CASE_CONFIGURE_URL = `${CASES_URL}/configure`; export const CASE_CONFIGURE_CONNECTORS_URL = `${CASE_CONFIGURE_URL}/connectors`; +export const CASE_CONFIGURE_CONNECTOR_DETAILS_URL = `${CASE_CONFIGURE_CONNECTORS_URL}/{connector_id}`; +export const CASE_CONFIGURE_PUSH_URL = `${CASE_CONFIGURE_CONNECTOR_DETAILS_URL}/push`; export const CASE_COMMENTS_URL = `${CASE_DETAILS_URL}/comments`; export const CASE_COMMENT_DETAILS_URL = `${CASE_DETAILS_URL}/comments/{comment_id}`; export const CASE_REPORTERS_URL = `${CASES_URL}/reporters`; diff --git a/x-pack/plugins/case/server/client/configure/get_fields.ts b/x-pack/plugins/case/server/client/configure/get_fields.ts new file mode 100644 index 0000000000000..9f8988d6355ac --- /dev/null +++ b/x-pack/plugins/case/server/client/configure/get_fields.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from '@hapi/boom'; + +import { GetFieldsResponse } from '../../../common/api'; +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/case/server/client/configure/get_mappings.ts b/x-pack/plugins/case/server/client/configure/get_mappings.ts new file mode 100644 index 0000000000000..4b58bd085f391 --- /dev/null +++ b/x-pack/plugins/case/server/client/configure/get_mappings.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ConnectorMappingsAttributes, ConnectorTypes } from '../../../common/api'; +import { CaseClientFactoryArguments, MappingsClient } from '../types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server/saved_objects'; + +export const getMappings = ({ + savedObjectsClient, + connectorMappingsService, +}: CaseClientFactoryArguments) => async ({ + actionsClient, + caseClient, + connectorType, + connectorId, +}: MappingsClient): Promise => { + if (connectorType === ConnectorTypes.none) { + return []; + } + const myConnectorMappings = await connectorMappingsService.find({ + client: savedObjectsClient, + options: { + hasReference: { + type: ACTION_SAVED_OBJECT_TYPE, + id: connectorId, + }, + }, + }); + let theMapping; + // Create connector mappings if there are none + if (myConnectorMappings.total === 0) { + const res = await caseClient.getFields({ + actionsClient, + connectorId, + connectorType, + }); + theMapping = await connectorMappingsService.post({ + client: savedObjectsClient, + attributes: { + mappings: res.defaultMappings, + }, + references: [ + { + type: ACTION_SAVED_OBJECT_TYPE, + name: `associated-${ACTION_SAVED_OBJECT_TYPE}`, + id: connectorId, + }, + ], + }); + } else { + theMapping = myConnectorMappings.saved_objects[0]; + } + return theMapping ? theMapping.attributes.mappings : []; +}; diff --git a/x-pack/plugins/case/server/client/configure/utils.test.ts b/x-pack/plugins/case/server/client/configure/utils.test.ts new file mode 100644 index 0000000000000..91c8259cb2c55 --- /dev/null +++ b/x-pack/plugins/case/server/client/configure/utils.test.ts @@ -0,0 +1,545 @@ +/* + * 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 { + JiraGetFieldsResponse, + ResilientGetFieldsResponse, + ServiceNowGetFieldsResponse, +} from '../../../../actions/server/types'; +import { formatFields } from './utils'; +import { ConnectorTypes } from '../../../common/api/connectors'; + +const jiraFields: JiraGetFieldsResponse = { + summary: { + required: true, + allowedValues: [], + defaultValue: {}, + schema: { + type: 'string', + }, + name: 'Summary', + }, + issuetype: { + required: true, + allowedValues: [ + { + self: 'https://siem-kibana.atlassian.net/rest/api/2/issuetype/10023', + id: '10023', + description: 'A problem or error.', + iconUrl: + 'https://siem-kibana.atlassian.net/secure/viewavatar?size=medium&avatarId=10303&avatarType=issuetype', + name: 'Bug', + subtask: false, + avatarId: 10303, + }, + ], + defaultValue: {}, + schema: { + type: 'issuetype', + }, + name: 'Issue Type', + }, + attachment: { + required: false, + allowedValues: [], + defaultValue: {}, + schema: { + type: 'array', + items: 'attachment', + }, + name: 'Attachment', + }, + duedate: { + required: false, + allowedValues: [], + defaultValue: {}, + schema: { + type: 'date', + }, + name: 'Due date', + }, + description: { + required: false, + allowedValues: [], + defaultValue: {}, + schema: { + type: 'string', + }, + name: 'Description', + }, + project: { + required: true, + allowedValues: [ + { + self: 'https://siem-kibana.atlassian.net/rest/api/2/project/10015', + id: '10015', + key: 'RJ2', + name: 'RJ2', + projectTypeKey: 'business', + simplified: false, + avatarUrls: { + '48x48': + 'https://siem-kibana.atlassian.net/secure/projectavatar?pid=10015&avatarId=10412', + '24x24': + 'https://siem-kibana.atlassian.net/secure/projectavatar?size=small&s=small&pid=10015&avatarId=10412', + '16x16': + 'https://siem-kibana.atlassian.net/secure/projectavatar?size=xsmall&s=xsmall&pid=10015&avatarId=10412', + '32x32': + 'https://siem-kibana.atlassian.net/secure/projectavatar?size=medium&s=medium&pid=10015&avatarId=10412', + }, + }, + ], + defaultValue: {}, + schema: { + type: 'project', + }, + name: 'Project', + }, + assignee: { + required: false, + allowedValues: [], + defaultValue: {}, + schema: { + type: 'user', + }, + name: 'Assignee', + }, + labels: { + required: false, + allowedValues: [], + defaultValue: {}, + schema: { + type: 'array', + items: 'string', + }, + name: 'Labels', + }, +}; +const resilientFields: ResilientGetFieldsResponse = [ + { input_type: 'text', name: 'addr', read_only: false, text: 'Address' }, + { + input_type: 'boolean', + name: 'alberta_health_risk_assessment', + read_only: false, + text: 'Alberta Health Risk Assessment', + }, + { input_type: 'number', name: 'hard_liability', read_only: true, text: 'Assessed Liability' }, + { input_type: 'text', name: 'city', read_only: false, text: 'City' }, + { input_type: 'select', name: 'country', read_only: false, text: 'Country/Region' }, + { input_type: 'select_owner', name: 'creator_id', read_only: true, text: 'Created By' }, + { input_type: 'select', name: 'crimestatus_id', read_only: false, text: 'Criminal Activity' }, + { input_type: 'boolean', name: 'data_encrypted', read_only: false, text: 'Data Encrypted' }, + { input_type: 'select', name: 'data_format', read_only: false, text: 'Data Format' }, + { input_type: 'datetimepicker', name: 'end_date', read_only: true, text: 'Date Closed' }, + { input_type: 'datetimepicker', name: 'create_date', read_only: true, text: 'Date Created' }, + { + input_type: 'datetimepicker', + name: 'determined_date', + read_only: false, + text: 'Date Determined', + }, + { + input_type: 'datetimepicker', + name: 'discovered_date', + read_only: false, + required: 'always', + text: 'Date Discovered', + }, + { input_type: 'datetimepicker', name: 'start_date', read_only: false, text: 'Date Occurred' }, + { input_type: 'select', name: 'exposure_dept_id', read_only: false, text: 'Department' }, + { input_type: 'textarea', name: 'description', read_only: false, text: 'Description' }, + { input_type: 'boolean', name: 'employee_involved', read_only: false, text: 'Employee Involved' }, + { input_type: 'boolean', name: 'data_contained', read_only: false, text: 'Exposure Resolved' }, + { input_type: 'select', name: 'exposure_type_id', read_only: false, text: 'Exposure Type' }, + { + input_type: 'multiselect', + name: 'gdpr_breach_circumstances', + read_only: false, + text: 'GDPR Breach Circumstances', + }, + { input_type: 'select', name: 'gdpr_breach_type', read_only: false, text: 'GDPR Breach Type' }, + { + input_type: 'textarea', + name: 'gdpr_breach_type_comment', + read_only: false, + text: 'GDPR Breach Type Comment', + }, + { input_type: 'select', name: 'gdpr_consequences', read_only: false, text: 'GDPR Consequences' }, + { + input_type: 'textarea', + name: 'gdpr_consequences_comment', + read_only: false, + text: 'GDPR Consequences Comment', + }, + { + input_type: 'select', + name: 'gdpr_final_assessment', + read_only: false, + text: 'GDPR Final Assessment', + }, + { + input_type: 'textarea', + name: 'gdpr_final_assessment_comment', + read_only: false, + text: 'GDPR Final Assessment Comment', + }, + { + input_type: 'select', + name: 'gdpr_identification', + read_only: false, + text: 'GDPR Identification', + }, + { + input_type: 'textarea', + name: 'gdpr_identification_comment', + read_only: false, + text: 'GDPR Identification Comment', + }, + { + input_type: 'select', + name: 'gdpr_personal_data', + read_only: false, + text: 'GDPR Personal Data', + }, + { + input_type: 'textarea', + name: 'gdpr_personal_data_comment', + read_only: false, + text: 'GDPR Personal Data Comment', + }, + { + input_type: 'boolean', + name: 'gdpr_subsequent_notification', + read_only: false, + text: 'GDPR Subsequent Notification', + }, + { input_type: 'number', name: 'id', read_only: true, text: 'ID' }, + { input_type: 'boolean', name: 'impact_likely', read_only: false, text: 'Impact Likely' }, + { + input_type: 'boolean', + name: 'ny_impact_likely', + read_only: false, + text: 'Impact Likely for New York', + }, + { + input_type: 'boolean', + name: 'or_impact_likely', + read_only: false, + text: 'Impact Likely for Oregon', + }, + { + input_type: 'boolean', + name: 'wa_impact_likely', + read_only: false, + text: 'Impact Likely for Washington', + }, + { input_type: 'boolean', name: 'confirmed', read_only: false, text: 'Incident Disposition' }, + { input_type: 'multiselect', name: 'incident_type_ids', read_only: false, text: 'Incident Type' }, + { + input_type: 'text', + name: 'exposure_individual_name', + read_only: false, + text: 'Individual Name', + }, + { + input_type: 'select', + name: 'harmstatus_id', + read_only: false, + text: 'Is harm/risk/misuse foreseeable?', + }, + { input_type: 'text', name: 'jurisdiction_name', read_only: false, text: 'Jurisdiction' }, + { + input_type: 'datetimepicker', + name: 'inc_last_modified_date', + read_only: true, + text: 'Last Modified', + }, + { + input_type: 'multiselect', + name: 'gdpr_lawful_data_processing_categories', + read_only: false, + text: 'Lawful Data Processing Categories', + }, + { input_type: 'multiselect_members', name: 'members', read_only: false, text: 'Members' }, + { input_type: 'text', name: 'name', read_only: false, required: 'always', text: 'Name' }, + { input_type: 'boolean', name: 'negative_pr_likely', read_only: false, text: 'Negative PR' }, + { input_type: 'datetimepicker', name: 'due_date', read_only: true, text: 'Next Due Date' }, + { + input_type: 'multiselect', + name: 'nist_attack_vectors', + read_only: false, + text: 'NIST Attack Vectors', + }, + { input_type: 'select', name: 'org_handle', read_only: true, text: 'Organization' }, + { input_type: 'select_owner', name: 'owner_id', read_only: false, text: 'Owner' }, + { input_type: 'select', name: 'phase_id', read_only: true, text: 'Phase' }, + { + input_type: 'select', + name: 'pipeda_other_factors', + read_only: false, + text: 'PIPEDA Other Factors', + }, + { + input_type: 'textarea', + name: 'pipeda_other_factors_comment', + read_only: false, + text: 'PIPEDA Other Factors Comment', + }, + { + input_type: 'select', + name: 'pipeda_overall_assessment', + read_only: false, + text: 'PIPEDA Overall Assessment', + }, + { + input_type: 'textarea', + name: 'pipeda_overall_assessment_comment', + read_only: false, + text: 'PIPEDA Overall Assessment Comment', + }, + { + input_type: 'select', + name: 'pipeda_probability_of_misuse', + read_only: false, + text: 'PIPEDA Probability of Misuse', + }, + { + input_type: 'textarea', + name: 'pipeda_probability_of_misuse_comment', + read_only: false, + text: 'PIPEDA Probability of Misuse Comment', + }, + { + input_type: 'select', + name: 'pipeda_sensitivity_of_pi', + read_only: false, + text: 'PIPEDA Sensitivity of PI', + }, + { + input_type: 'textarea', + name: 'pipeda_sensitivity_of_pi_comment', + read_only: false, + text: 'PIPEDA Sensitivity of PI Comment', + }, + { input_type: 'text', name: 'reporter', read_only: false, text: 'Reporting Individual' }, + { + input_type: 'select', + name: 'resolution_id', + read_only: false, + required: 'close', + text: 'Resolution', + }, + { + input_type: 'textarea', + name: 'resolution_summary', + read_only: false, + required: 'close', + text: 'Resolution Summary', + }, + { input_type: 'select', name: 'gdpr_harm_risk', read_only: false, text: 'Risk of Harm' }, + { input_type: 'select', name: 'severity_code', read_only: false, text: 'Severity' }, + { input_type: 'boolean', name: 'inc_training', read_only: true, text: 'Simulation' }, + { input_type: 'multiselect', name: 'data_source_ids', read_only: false, text: 'Source of Data' }, + { input_type: 'select', name: 'state', read_only: false, text: 'State' }, + { input_type: 'select', name: 'plan_status', read_only: false, text: 'Status' }, + { input_type: 'select', name: 'exposure_vendor_id', read_only: false, text: 'Vendor' }, + { + input_type: 'boolean', + name: 'data_compromised', + read_only: false, + text: 'Was personal information or personal data involved?', + }, + { + input_type: 'select', + name: 'workspace', + read_only: false, + required: 'always', + text: 'Workspace', + }, + { input_type: 'text', name: 'zip', read_only: false, text: 'Zip' }, +]; +const serviceNowFields: ServiceNowGetFieldsResponse = [ + { + column_label: 'Approval', + mandatory: 'false', + max_length: '40', + element: 'approval', + }, + { + column_label: 'Close notes', + mandatory: 'false', + max_length: '4000', + element: 'close_notes', + }, + { + column_label: 'Contact type', + mandatory: 'false', + max_length: '40', + element: 'contact_type', + }, + { + column_label: 'Correlation display', + mandatory: 'false', + max_length: '100', + element: 'correlation_display', + }, + { + column_label: 'Correlation ID', + mandatory: 'false', + max_length: '100', + element: 'correlation_id', + }, + { + column_label: 'Description', + mandatory: 'false', + max_length: '4000', + element: 'description', + }, + { + column_label: 'Number', + mandatory: 'false', + max_length: '40', + element: 'number', + }, + { + column_label: 'Short description', + mandatory: 'false', + max_length: '160', + element: 'short_description', + }, + { + column_label: 'Created by', + mandatory: 'false', + max_length: '40', + element: 'sys_created_by', + }, + { + column_label: 'Updated by', + mandatory: 'false', + max_length: '40', + element: 'sys_updated_by', + }, + { + column_label: 'Upon approval', + mandatory: 'false', + max_length: '40', + element: 'upon_approval', + }, + { + column_label: 'Upon reject', + mandatory: 'false', + max_length: '40', + element: 'upon_reject', + }, +]; + +const formatFieldsTestData = [ + { + expected: [ + { id: 'summary', name: 'Summary', required: true, type: 'text' }, + { id: 'description', name: 'Description', required: false, type: 'text' }, + ], + fields: jiraFields, + type: ConnectorTypes.jira, + }, + { + expected: [ + { id: 'addr', name: 'Address', required: false, type: 'text' }, + { id: 'city', name: 'City', required: false, type: 'text' }, + { id: 'description', name: 'Description', required: false, type: 'textarea' }, + { + id: 'gdpr_breach_type_comment', + name: 'GDPR Breach Type Comment', + required: false, + type: 'textarea', + }, + { + id: 'gdpr_consequences_comment', + name: 'GDPR Consequences Comment', + required: false, + type: 'textarea', + }, + { + id: 'gdpr_final_assessment_comment', + name: 'GDPR Final Assessment Comment', + required: false, + type: 'textarea', + }, + { + id: 'gdpr_identification_comment', + name: 'GDPR Identification Comment', + required: false, + type: 'textarea', + }, + { + id: 'gdpr_personal_data_comment', + name: 'GDPR Personal Data Comment', + required: false, + type: 'textarea', + }, + { id: 'exposure_individual_name', name: 'Individual Name', required: false, type: 'text' }, + { id: 'jurisdiction_name', name: 'Jurisdiction', required: false, type: 'text' }, + { id: 'name', name: 'Name', required: true, type: 'text' }, + { + id: 'pipeda_other_factors_comment', + name: 'PIPEDA Other Factors Comment', + required: false, + type: 'textarea', + }, + { + id: 'pipeda_overall_assessment_comment', + name: 'PIPEDA Overall Assessment Comment', + required: false, + type: 'textarea', + }, + { + id: 'pipeda_probability_of_misuse_comment', + name: 'PIPEDA Probability of Misuse Comment', + required: false, + type: 'textarea', + }, + { + id: 'pipeda_sensitivity_of_pi_comment', + name: 'PIPEDA Sensitivity of PI Comment', + required: false, + type: 'textarea', + }, + { id: 'reporter', name: 'Reporting Individual', required: false, type: 'text' }, + { id: 'resolution_summary', name: 'Resolution Summary', required: false, type: 'textarea' }, + { id: 'zip', name: 'Zip', required: false, type: 'text' }, + ], + fields: resilientFields, + type: ConnectorTypes.resilient, + }, + { + expected: [ + { id: 'approval', name: 'Approval', required: false, type: 'text' }, + { id: 'close_notes', name: 'Close notes', required: false, type: 'textarea' }, + { id: 'contact_type', name: 'Contact type', required: false, type: 'text' }, + { id: 'correlation_display', name: 'Correlation display', required: false, type: 'text' }, + { id: 'correlation_id', name: 'Correlation ID', required: false, type: 'text' }, + { id: 'description', name: 'Description', required: false, type: 'textarea' }, + { id: 'number', name: 'Number', required: false, type: 'text' }, + { id: 'short_description', name: 'Short description', required: false, type: 'text' }, + { id: 'sys_created_by', name: 'Created by', required: false, type: 'text' }, + { id: 'sys_updated_by', name: 'Updated by', required: false, type: 'text' }, + { id: 'upon_approval', name: 'Upon approval', required: false, type: 'text' }, + { id: 'upon_reject', name: 'Upon reject', required: false, type: 'text' }, + ], + fields: serviceNowFields, + type: ConnectorTypes.servicenow, + }, +]; +describe('client/configure/utils', () => { + describe('formatFields', () => { + formatFieldsTestData.forEach(({ expected, fields, type }) => { + it(`normalizes ${type} fields to common type ConnectorField`, () => { + const result = formatFields(fields, type); + expect(result).toEqual(expected); + }); + }); + }); +}); diff --git a/x-pack/plugins/case/server/client/configure/utils.ts b/x-pack/plugins/case/server/client/configure/utils.ts new file mode 100644 index 0000000000000..2fc6db8c86cbc --- /dev/null +++ b/x-pack/plugins/case/server/client/configure/utils.ts @@ -0,0 +1,169 @@ +/* + * 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 { + ConnectorField, + ConnectorMappingsAttributes, + ConnectorTypes, +} from '../../../common/api/connectors'; +import { + JiraGetFieldsResponse, + ResilientGetFieldsResponse, + ServiceNowGetFieldsResponse, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../actions/server/types'; + +const normalizeJiraFields = (jiraFields: JiraGetFieldsResponse): ConnectorField[] => + Object.keys(jiraFields).reduce( + (acc, data) => + jiraFields[data].schema.type === 'string' + ? [ + ...acc, + { + id: data, + name: jiraFields[data].name, + required: jiraFields[data].required, + type: 'text', + }, + ] + : acc, + [] + ); + +const normalizeResilientFields = (resilientFields: ResilientGetFieldsResponse): ConnectorField[] => + resilientFields.reduce( + (acc: ConnectorField[], data) => + (data.input_type === 'textarea' || data.input_type === 'text') && !data.read_only + ? [ + ...acc, + { + id: data.name, + name: data.text, + required: data.required === 'always', + type: data.input_type, + }, + ] + : acc, + [] + ); +const normalizeServiceNowFields = (snFields: ServiceNowGetFieldsResponse): ConnectorField[] => + snFields.reduce( + (acc, data) => [ + ...acc, + { + id: data.element, + name: data.column_label, + required: data.mandatory === 'true', + type: parseFloat(data.max_length) > 160 ? 'textarea' : 'text', + }, + ], + [] + ); + +export const formatFields = (theData: unknown, theType: string): ConnectorField[] => { + switch (theType) { + case ConnectorTypes.jira: + return normalizeJiraFields(theData as JiraGetFieldsResponse); + case ConnectorTypes.resilient: + return normalizeResilientFields(theData as ResilientGetFieldsResponse); + case ConnectorTypes.servicenow: + return normalizeServiceNowFields(theData as ServiceNowGetFieldsResponse); + default: + return []; + } +}; +const findTextField = (fields: ConnectorField[]): string => + ( + fields.find((field: ConnectorField) => field.type === 'text' && field.required) ?? + fields.find((field: ConnectorField) => field.type === 'text') + )?.id ?? ''; +const findTextAreaField = (fields: ConnectorField[]): string => + ( + fields.find((field: ConnectorField) => field.type === 'textarea' && field.required) ?? + fields.find((field: ConnectorField) => field.type === 'textarea') ?? + fields.find((field: ConnectorField) => field.type === 'text') + )?.id ?? ''; + +const getPreferredFields = (theType: string) => { + let title: string = ''; + let description: string = ''; + if (theType === ConnectorTypes.jira) { + title = 'summary'; + description = 'description'; + } else if (theType === ConnectorTypes.resilient) { + title = 'name'; + description = 'description'; + } else if (theType === ConnectorTypes.servicenow) { + title = 'short_description'; + description = 'description'; + } + return { title, description }; +}; + +const getRemainingFields = (fields: ConnectorField[], titleTarget: string) => + fields.filter((field: ConnectorField) => field.id !== titleTarget); + +const getDynamicFields = (fields: ConnectorField[], dynamicTitle = findTextField(fields)) => { + const remainingFields = getRemainingFields(fields, dynamicTitle); + const dynamicDescription = findTextAreaField(remainingFields); + return { + description: dynamicDescription, + title: dynamicTitle, + }; +}; + +const getField = (fields: ConnectorField[], fieldId: string) => + fields.find((field: ConnectorField) => field.id === fieldId); + +// if dynamic title is not required and preferred is, true +const shouldTargetBePreferred = ( + fields: ConnectorField[], + dynamic: string, + preferred: string +): boolean => { + if (dynamic !== preferred) { + const dynamicT = getField(fields, dynamic); + const preferredT = getField(fields, preferred); + return preferredT != null && !(dynamicT?.required && !preferredT.required); + } + return false; +}; +export const createDefaultMapping = ( + fields: ConnectorField[], + theType: string +): ConnectorMappingsAttributes[] => { + const { description: dynamicDescription, title: dynamicTitle } = getDynamicFields(fields); + const { description: preferredDescription, title: preferredTitle } = getPreferredFields(theType); + let titleTarget = dynamicTitle; + let descriptionTarget = dynamicDescription; + if (preferredTitle.length > 0 && preferredDescription.length > 0) { + if (shouldTargetBePreferred(fields, dynamicTitle, preferredTitle)) { + const { description: dynamicDescriptionOverwrite } = getDynamicFields(fields, preferredTitle); + titleTarget = preferredTitle; + descriptionTarget = dynamicDescriptionOverwrite; + } + if (shouldTargetBePreferred(fields, descriptionTarget, preferredDescription)) { + descriptionTarget = preferredDescription; + } + } + return [ + { + source: 'title', + target: titleTarget, + action_type: 'overwrite', + }, + { + source: 'description', + target: descriptionTarget, + action_type: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + action_type: 'append', + }, + ]; +}; diff --git a/x-pack/plugins/case/server/client/index.test.ts b/x-pack/plugins/case/server/client/index.test.ts index ef4491204d9f5..0c54db11287d8 100644 --- a/x-pack/plugins/case/server/client/index.test.ts +++ b/x-pack/plugins/case/server/client/index.test.ts @@ -8,6 +8,7 @@ import { KibanaRequest, RequestHandlerContext } from 'kibana/server'; import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { createCaseClient } from '.'; import { + connectorMappingsServiceMock, createCaseServiceMock, createConfigureServiceMock, createUserActionServiceMock, @@ -24,12 +25,13 @@ jest.mock('./cases/update'); jest.mock('./comments/add'); jest.mock('./alerts/update_status'); -const caseService = createCaseServiceMock(); const caseConfigureService = createConfigureServiceMock(); -const userActionService = createUserActionServiceMock(); const alertsService = createAlertServiceMock(); -const savedObjectsClient = savedObjectsClientMock.create(); +const caseService = createCaseServiceMock(); +const connectorMappingsService = connectorMappingsServiceMock(); const request = {} as KibanaRequest; +const savedObjectsClient = savedObjectsClientMock.create(); +const userActionService = createUserActionServiceMock(); const context = {} as RequestHandlerContext; const createMock = create as jest.Mock; @@ -40,53 +42,58 @@ const updateAlertsStatusMock = updateAlertsStatus as jest.Mock; describe('createCaseClient()', () => { test('it creates the client correctly', async () => { createCaseClient({ - savedObjectsClient, - request, + alertsService, caseConfigureService, caseService, - userActionService, - alertsService, + connectorMappingsService, context, + request, + savedObjectsClient, + userActionService, }); expect(createMock).toHaveBeenCalledWith({ - savedObjectsClient, - request, + alertsService, caseConfigureService, caseService, - userActionService, - alertsService, + connectorMappingsService, context, + request, + savedObjectsClient, + userActionService, }); expect(updateMock).toHaveBeenCalledWith({ - savedObjectsClient, - request, + alertsService, caseConfigureService, caseService, - userActionService, - alertsService, + connectorMappingsService, context, + request, + savedObjectsClient, + userActionService, }); expect(addCommentMock).toHaveBeenCalledWith({ - savedObjectsClient, - request, + alertsService, caseConfigureService, caseService, - userActionService, - alertsService, + connectorMappingsService, context, + request, + savedObjectsClient, + userActionService, }); expect(updateAlertsStatusMock).toHaveBeenCalledWith({ - savedObjectsClient, - request, + alertsService, caseConfigureService, caseService, - userActionService, - alertsService, + connectorMappingsService, context, + request, + savedObjectsClient, + userActionService, }); }); }); diff --git a/x-pack/plugins/case/server/client/index.ts b/x-pack/plugins/case/server/client/index.ts index bf43921b46466..70eb3282dd243 100644 --- a/x-pack/plugins/case/server/client/index.ts +++ b/x-pack/plugins/case/server/client/index.ts @@ -8,55 +8,73 @@ import { CaseClientFactoryArguments, CaseClient } from './types'; import { create } from './cases/create'; import { update } from './cases/update'; import { addComment } from './comments/add'; +import { getFields } from './configure/get_fields'; +import { getMappings } from './configure/get_mappings'; import { updateAlertsStatus } from './alerts/update_status'; export { CaseClient } from './types'; export const createCaseClient = ({ - savedObjectsClient, - request, caseConfigureService, caseService, + connectorMappingsService, + request, + savedObjectsClient, userActionService, alertsService, context, }: CaseClientFactoryArguments): CaseClient => { return { create: create({ - savedObjectsClient, - request, + alertsService, caseConfigureService, caseService, - userActionService, - alertsService, + connectorMappingsService, context, + request, + savedObjectsClient, + userActionService, }), update: update({ - savedObjectsClient, - request, + alertsService, caseConfigureService, caseService, - userActionService, - alertsService, + connectorMappingsService, context, + request, + savedObjectsClient, + userActionService, }), addComment: addComment({ - savedObjectsClient, - request, + alertsService, caseConfigureService, caseService, + connectorMappingsService, + context, + request, + savedObjectsClient, userActionService, + }), + getFields: getFields(), + getMappings: getMappings({ alertsService, + caseConfigureService, + caseService, + connectorMappingsService, context, + request, + savedObjectsClient, + userActionService, }), updateAlertsStatus: updateAlertsStatus({ - savedObjectsClient, - request, + alertsService, caseConfigureService, caseService, - userActionService, - alertsService, + connectorMappingsService, context, + request, + savedObjectsClient, + userActionService, }), }; }; diff --git a/x-pack/plugins/case/server/client/mocks.ts b/x-pack/plugins/case/server/client/mocks.ts index dd4e8b52b4dc6..54af9bee2b316 100644 --- a/x-pack/plugins/case/server/client/mocks.ts +++ b/x-pack/plugins/case/server/client/mocks.ts @@ -8,10 +8,11 @@ import { KibanaRequest, RequestHandlerContext } from 'kibana/server'; import { loggingSystemMock, elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { actionsClientMock } from '../../../actions/server/mocks'; import { - CaseService, + AlertService, CaseConfigureService, + CaseService, CaseUserActionServiceSetup, - AlertService, + ConnectorMappingsService, } from '../services'; import { CaseClient } from './types'; import { authenticationMock } from '../routes/api/__fixtures__'; @@ -20,9 +21,11 @@ import { getActions } from '../routes/api/__mocks__/request_responses'; export type CaseClientMock = jest.Mocked; export const createCaseClientMock = (): CaseClientMock => ({ + addComment: jest.fn(), create: jest.fn(), + getFields: jest.fn(), + getMappings: jest.fn(), update: jest.fn(), - addComment: jest.fn(), updateAlertsStatus: jest.fn(), }); @@ -41,11 +44,14 @@ export const createCaseClientWithMockSavedObjectsClient = async ( const caseServicePlugin = new CaseService(log); const caseConfigureServicePlugin = new CaseConfigureService(log); + const connectorMappingsServicePlugin = new ConnectorMappingsService(log); const caseService = await caseServicePlugin.setup({ authentication: badAuth ? authenticationMock.createInvalid() : authenticationMock.create(), }); const caseConfigureService = await caseConfigureServicePlugin.setup(); + + const connectorMappingsService = await connectorMappingsServicePlugin.setup(); const userActionService = { postUserActions: jest.fn(), getUserActions: jest.fn(), @@ -75,11 +81,11 @@ export const createCaseClientWithMockSavedObjectsClient = async ( request, caseService, caseConfigureService, + connectorMappingsService, userActionService, alertsService, context, }); - return { client: caseClient, services: { userActionService }, diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts index a9e8494c43dbc..ec83f1ec1ff7d 100644 --- a/x-pack/plugins/case/server/client/types.ts +++ b/x-pack/plugins/case/server/client/types.ts @@ -5,13 +5,16 @@ */ import { KibanaRequest, SavedObjectsClientContract, RequestHandlerContext } from 'kibana/server'; +import { ActionsClient } from '../../../actions/server'; import { CasePostRequest, - CasesPatchRequest, - CommentRequest, CaseResponse, + CasesPatchRequest, CasesResponse, CaseStatuses, + CommentRequest, + ConnectorMappingsAttributes, + GetFieldsResponse, } from '../../common/api'; import { CaseConfigureServiceSetup, @@ -19,7 +22,7 @@ import { CaseUserActionServiceSetup, AlertServiceContract, } from '../services'; - +import { ConnectorMappingsServiceSetup } from '../services/connector_mappings'; export interface CaseClientCreate { theCase: CasePostRequest; } @@ -43,18 +46,33 @@ export interface CaseClientUpdateAlertsStatus { type PartialExceptFor = Partial & Pick; export interface CaseClientFactoryArguments { - savedObjectsClient: SavedObjectsClientContract; - request: KibanaRequest; caseConfigureService: CaseConfigureServiceSetup; caseService: CaseServiceSetup; + connectorMappingsService: ConnectorMappingsServiceSetup; + request: KibanaRequest; + savedObjectsClient: SavedObjectsClientContract; userActionService: CaseUserActionServiceSetup; alertsService: AlertServiceContract; context?: PartialExceptFor; } +export interface ConfigureFields { + actionsClient: ActionsClient; + connectorId: string; + connectorType: string; +} export interface CaseClient { + addComment: (args: CaseClientAddComment) => Promise; create: (args: CaseClientCreate) => Promise; + getFields: (args: ConfigureFields) => Promise; + getMappings: (args: MappingsClient) => Promise; update: (args: CaseClientUpdate) => Promise; - addComment: (args: CaseClientAddComment) => Promise; updateAlertsStatus: (args: CaseClientUpdateAlertsStatus) => Promise; } + +export interface MappingsClient { + actionsClient: ActionsClient; + caseClient: CaseClient; + connectorId: string; + connectorType: string; +} diff --git a/x-pack/plugins/case/server/connectors/case/index.test.ts b/x-pack/plugins/case/server/connectors/case/index.test.ts index 9f5b186c0c687..442b23da87c96 100644 --- a/x-pack/plugins/case/server/connectors/case/index.test.ts +++ b/x-pack/plugins/case/server/connectors/case/index.test.ts @@ -11,6 +11,7 @@ import { actionsMock } from '../../../../actions/server/mocks'; import { validateParams } from '../../../../actions/server/lib'; import { ConnectorTypes, CommentType, CaseStatuses } from '../../../common/api'; import { + connectorMappingsServiceMock, createCaseServiceMock, createConfigureServiceMock, createUserActionServiceMock, @@ -35,12 +36,14 @@ describe('case connector', () => { const logger = loggingSystemMock.create().get() as jest.Mocked; const caseService = createCaseServiceMock(); const caseConfigureService = createConfigureServiceMock(); + const connectorMappingsService = connectorMappingsServiceMock(); const userActionService = createUserActionServiceMock(); const alertsService = createAlertServiceMock(); caseActionType = getActionType({ logger, caseService, caseConfigureService, + connectorMappingsService, userActionService, alertsService, }); diff --git a/x-pack/plugins/case/server/connectors/case/index.ts b/x-pack/plugins/case/server/connectors/case/index.ts index 48124b8ae32eb..2195786f718ab 100644 --- a/x-pack/plugins/case/server/connectors/case/index.ts +++ b/x-pack/plugins/case/server/connectors/case/index.ts @@ -29,6 +29,7 @@ export function getActionType({ logger, caseService, caseConfigureService, + connectorMappingsService, userActionService, alertsService, }: GetActionTypeParams): CaseActionType { @@ -41,11 +42,12 @@ export function getActionType({ params: CaseExecutorParamsSchema, }, executor: curry(executor)({ - logger, - caseService, + alertsService, caseConfigureService, + caseService, + connectorMappingsService, + logger, userActionService, - alertsService, }), }; } @@ -53,11 +55,12 @@ export function getActionType({ // action executor async function executor( { - logger, - caseService, + alertsService, caseConfigureService, + caseService, + connectorMappingsService, + logger, userActionService, - alertsService, }: GetActionTypeParams, execOptions: CaseActionTypeExecutorOptions ): Promise> { @@ -71,6 +74,7 @@ async function executor( request: {} as KibanaRequest, caseService, caseConfigureService, + connectorMappingsService, userActionService, alertsService, // TODO: When case connector is enabled we should figure out how to pass the context. diff --git a/x-pack/plugins/case/server/connectors/index.ts b/x-pack/plugins/case/server/connectors/index.ts index f373445719164..7fd09e61f2144 100644 --- a/x-pack/plugins/case/server/connectors/index.ts +++ b/x-pack/plugins/case/server/connectors/index.ts @@ -16,6 +16,7 @@ import { CaseServiceSetup, CaseConfigureServiceSetup, CaseUserActionServiceSetup, + ConnectorMappingsServiceSetup, AlertServiceContract, } from '../services'; @@ -26,6 +27,7 @@ export interface GetActionTypeParams { logger: Logger; caseService: CaseServiceSetup; caseConfigureService: CaseConfigureServiceSetup; + connectorMappingsService: ConnectorMappingsServiceSetup; userActionService: CaseUserActionServiceSetup; alertsService: AlertServiceContract; } @@ -46,6 +48,7 @@ export const registerConnectors = ({ logger, caseService, caseConfigureService, + connectorMappingsService, userActionService, alertsService, }: RegisterConnectorsArgs) => { @@ -54,6 +57,7 @@ export const registerConnectors = ({ logger, caseService, caseConfigureService, + connectorMappingsService, userActionService, alertsService, }) diff --git a/x-pack/plugins/case/server/index.ts b/x-pack/plugins/case/server/index.ts index d4f06c8a2304c..19f3a15396729 100644 --- a/x-pack/plugins/case/server/index.ts +++ b/x-pack/plugins/case/server/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from '../../../../src/core/server'; +import { PluginInitializerContext } from 'kibana/server'; import { ConfigSchema } from './config'; import { CasePlugin } from './plugin'; diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 8d508ce0b76b1..915656895e8c8 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -22,9 +22,10 @@ import { APP_ID } from '../common/constants'; import { ConfigType } from './config'; import { initCaseApi } from './routes/api'; import { - caseSavedObjectType, - caseConfigureSavedObjectType, caseCommentSavedObjectType, + caseConfigureSavedObjectType, + caseConnectorMappingsSavedObjectType, + caseSavedObjectType, caseUserActionSavedObjectType, } from './saved_object_types'; import { @@ -34,6 +35,8 @@ import { CaseServiceSetup, CaseUserActionService, CaseUserActionServiceSetup, + ConnectorMappingsService, + ConnectorMappingsServiceSetup, AlertService, AlertServiceContract, } from './services'; @@ -51,8 +54,9 @@ export interface PluginsSetup { export class CasePlugin { private readonly log: Logger; - private caseService?: CaseServiceSetup; private caseConfigureService?: CaseConfigureServiceSetup; + private caseService?: CaseServiceSetup; + private connectorMappingsService?: ConnectorMappingsServiceSetup; private userActionService?: CaseUserActionServiceSetup; private alertsService?: AlertService; @@ -67,9 +71,10 @@ export class CasePlugin { return; } - core.savedObjects.registerType(caseSavedObjectType); core.savedObjects.registerType(caseCommentSavedObjectType); core.savedObjects.registerType(caseConfigureSavedObjectType); + core.savedObjects.registerType(caseConnectorMappingsSavedObjectType); + core.savedObjects.registerType(caseSavedObjectType); core.savedObjects.registerType(caseUserActionSavedObjectType); this.log.debug( @@ -82,6 +87,7 @@ export class CasePlugin { authentication: plugins.security != null ? plugins.security.authc : null, }); this.caseConfigureService = await new CaseConfigureService(this.log).setup(); + this.connectorMappingsService = await new ConnectorMappingsService(this.log).setup(); this.userActionService = await new CaseUserActionService(this.log).setup(); this.alertsService = new AlertService(); @@ -91,6 +97,7 @@ export class CasePlugin { core, caseService: this.caseService, caseConfigureService: this.caseConfigureService, + connectorMappingsService: this.connectorMappingsService, userActionService: this.userActionService, alertsService: this.alertsService, }) @@ -100,6 +107,7 @@ export class CasePlugin { initCaseApi({ caseService: this.caseService, caseConfigureService: this.caseConfigureService, + connectorMappingsService: this.connectorMappingsService, userActionService: this.userActionService, router, }); @@ -109,6 +117,7 @@ export class CasePlugin { logger: this.log, caseService: this.caseService, caseConfigureService: this.caseConfigureService, + connectorMappingsService: this.connectorMappingsService, userActionService: this.userActionService, alertsService: this.alertsService, }); @@ -127,6 +136,7 @@ export class CasePlugin { request, caseService: this.caseService!, caseConfigureService: this.caseConfigureService!, + connectorMappingsService: this.connectorMappingsService!, userActionService: this.userActionService!, alertsService: this.alertsService!, context, @@ -146,12 +156,14 @@ export class CasePlugin { core, caseService, caseConfigureService, + connectorMappingsService, userActionService, alertsService, }: { core: CoreSetup; caseService: CaseServiceSetup; caseConfigureService: CaseConfigureServiceSetup; + connectorMappingsService: ConnectorMappingsServiceSetup; userActionService: CaseUserActionServiceSetup; alertsService: AlertServiceContract; }): IContextProvider, typeof APP_ID> => { @@ -163,6 +175,7 @@ export class CasePlugin { savedObjectsClient: savedObjects.getScopedClient(request), caseService, caseConfigureService, + connectorMappingsService, userActionService, alertsService, request, diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts index 8bbd419e6315b..1335d6107744c 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts @@ -15,16 +15,19 @@ import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, CASE_CONFIGURE_SAVED_OBJECT, + CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, } from '../../../saved_object_types'; export const createMockSavedObjectsRepository = ({ caseSavedObject = [], caseCommentSavedObject = [], caseConfigureSavedObject = [], + caseMappingsSavedObject = [], }: { caseSavedObject?: any[]; caseCommentSavedObject?: any[]; caseConfigureSavedObject?: any[]; + caseMappingsSavedObject?: any[]; }) => { const mockSavedObjectsClientContract = ({ bulkGet: jest.fn((objects: SavedObjectsBulkGetObject[]) => { @@ -103,6 +106,14 @@ export const createMockSavedObjectsRepository = ({ ) { throw SavedObjectsErrorHelpers.createGenericNotFoundError('Error thrown for testing'); } + if (findArgs.type === CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT && caseMappingsSavedObject[0]) { + return { + page: 1, + per_page: 5, + total: 1, + saved_objects: caseMappingsSavedObject, + }; + } if (findArgs.type === CASE_CONFIGURE_SAVED_OBJECT) { return { diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts index 8fde66ea82019..7c28ebd24c668 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts @@ -5,7 +5,7 @@ */ import { loggingSystemMock, httpServiceMock } from '../../../../../../../src/core/server/mocks'; -import { CaseService, CaseConfigureService } from '../../../services'; +import { CaseService, CaseConfigureService, ConnectorMappingsService } from '../../../services'; import { authenticationMock } from '../__fixtures__'; import { RouteDeps } from '../types'; @@ -21,15 +21,18 @@ export const createRoute = async ( const caseServicePlugin = new CaseService(log); const caseConfigureServicePlugin = new CaseConfigureService(log); + const connectorMappingsServicePlugin = new ConnectorMappingsService(log); const caseService = await caseServicePlugin.setup({ authentication: badAuth ? authenticationMock.createInvalid() : authenticationMock.create(), }); const caseConfigureService = await caseConfigureServicePlugin.setup(); + const connectorMappingsService = await connectorMappingsServicePlugin.setup(); api({ caseConfigureService, caseService, + connectorMappingsService, router, userActionService: { postUserActions: jest.fn(), diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts index 645673fdee756..45ccb4f2c539f 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -6,13 +6,16 @@ import { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; import { - ESCasesConfigureAttributes, + CaseStatuses, CommentAttributes, - ESCaseAttributes, - ConnectorTypes, CommentType, - CaseStatuses, + ConnectorMappings, + ConnectorTypes, + ESCaseAttributes, + ESCasesConfigureAttributes, } from '../../../../common/api'; +import { mappings } from '../cases/configure/mock'; +import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../../saved_object_types'; export const mockCases: Array> = [ { @@ -386,3 +389,23 @@ export const mockCaseConfigureFind: Array> = [ + { + type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, + id: 'mock-mappings-1', + attributes: { + mappings, + }, + references: [], + }, +]; + +export const mockCaseMappingsFind: Array> = [ + { + page: 1, + per_page: 5, + total: mockCaseConfigure.length, + saved_objects: [{ ...mockCaseMappings[0], score: 0 }], + }, +]; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts index dcae1c6083eb6..b2d232dbb7cca 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts @@ -8,7 +8,12 @@ import { RequestHandlerContext, KibanaRequest } from 'src/core/server'; import { loggingSystemMock, elasticsearchServiceMock } from 'src/core/server/mocks'; import { actionsClientMock } from '../../../../../actions/server/mocks'; import { createCaseClient } from '../../../client'; -import { CaseService, CaseConfigureService, AlertService } from '../../../services'; +import { + AlertService, + CaseService, + CaseConfigureService, + ConnectorMappingsService, +} from '../../../services'; import { getActions } from '../__mocks__/request_responses'; import { authenticationMock } from '../__fixtures__'; @@ -20,6 +25,7 @@ export const createRouteContext = async (client: any, badAuth = false) => { const caseServicePlugin = new CaseService(log); const caseConfigureServicePlugin = new CaseConfigureService(log); + const connectorMappingsServicePlugin = new ConnectorMappingsService(log); const caseService = await caseServicePlugin.setup({ authentication: badAuth ? authenticationMock.createInvalid() : authenticationMock.create(), @@ -45,11 +51,13 @@ export const createRouteContext = async (client: any, badAuth = false) => { }, } as unknown) as RequestHandlerContext; + const connectorMappingsService = await connectorMappingsServicePlugin.setup(); const caseClient = createCaseClient({ savedObjectsClient: client, request: {} as KibanaRequest, caseService, caseConfigureService, + connectorMappingsService, userActionService: { postUserActions: jest.fn(), getUserActions: jest.fn(), diff --git a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts index 209fa11116c56..b6da21927e342 100644 --- a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts +++ b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts @@ -40,27 +40,7 @@ export const getActions = (): FindActionResult[] => [ actionTypeId: '.servicenow', name: 'ServiceNow', config: { - incidentConfiguration: { - mapping: [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, apiUrl: 'https://dev102283.service-now.com', - isCaseOwned: true, }, isPreconfigured: false, referencedByCount: 0, @@ -70,25 +50,6 @@ export const getActions = (): FindActionResult[] => [ actionTypeId: '.jira', name: 'Connector without isCaseOwned', config: { - incidentConfiguration: { - mapping: [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, apiUrl: 'https://elastic.jira.com', }, isPreconfigured: false, diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts index cc4f208758369..d75f42f6e486b 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts @@ -11,11 +11,13 @@ import { createMockSavedObjectsRepository, createRoute, createRouteContext, + mockCaseConfigure, + mockCaseMappings, } from '../../__fixtures__'; -import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; import { initGetCaseConfigure } from './get_configure'; import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; +import { mappings } from './mock'; describe('GET configuration', () => { let routeHandler: RequestHandler; @@ -32,6 +34,7 @@ describe('GET configuration', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }) ); @@ -39,6 +42,7 @@ describe('GET configuration', () => { expect(res.status).toEqual(200); expect(res.payload).toEqual({ ...mockCaseConfigure[0].attributes, + mappings, version: mockCaseConfigure[0].version, }); }); @@ -52,6 +56,7 @@ describe('GET configuration', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [{ ...mockCaseConfigure[0], version: undefined }], + caseMappingsSavedObject: mockCaseMappings, }) ); @@ -71,6 +76,7 @@ describe('GET configuration', () => { email: 'testemail@elastic.co', username: 'elastic', }, + mappings, updated_at: '2020-04-09T09:43:51.778Z', updated_by: { full_name: 'elastic', diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts index 9b38524626290..615d4b0de17e8 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CaseConfigureResponseRt } from '../../../../../common/api'; +import Boom from '@hapi/boom'; +import { CaseConfigureResponseRt, ConnectorMappingsAttributes } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; @@ -24,6 +25,23 @@ export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps const { connector, ...caseConfigureWithoutConnector } = myCaseConfigure.saved_objects[0] ?.attributes ?? { connector: null }; + let mappings: ConnectorMappingsAttributes[] = []; + if (connector != null) { + if (!context.case) { + throw Boom.badRequest('RouteHandlerContext is not registered for cases'); + } + const caseClient = context.case.getCaseClient(); + const actionsClient = await context.actions?.getActionsClient(); + if (actionsClient == null) { + throw Boom.notFound('Action client have not been found'); + } + mappings = await caseClient.getMappings({ + actionsClient, + caseClient, + connectorId: connector.id, + connectorType: connector.type, + }); + } return response.ok({ body: @@ -31,6 +49,7 @@ export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps ? CaseConfigureResponseRt.encode({ ...caseConfigureWithoutConnector, connector: transformESConnectorToCaseConnector(connector), + mappings, version: myCaseConfigure.saved_objects[0].version ?? '', }) : {}, diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts index 2eab4ac756361..c77d2bd45a795 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts @@ -11,11 +11,13 @@ import { createMockSavedObjectsRepository, createRoute, createRouteContext, + mockCaseConfigure, + mockCaseMappings, } from '../../__fixtures__'; -import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; import { initCaseConfigureGetActionConnector } from './get_connectors'; import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../common/constants'; +import { getActions } from '../../__mocks__/request_responses'; describe('GET connectors', () => { let routeHandler: RequestHandler; @@ -32,72 +34,16 @@ describe('GET connectors', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }) ); const res = await routeHandler(context, req, kibanaResponseFactory); expect(res.status).toEqual(200); - expect(res.payload).toEqual([ - { - id: '123', - actionTypeId: '.servicenow', - name: 'ServiceNow', - config: { - incidentConfiguration: { - mapping: [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, - apiUrl: 'https://dev102283.service-now.com', - isCaseOwned: true, - }, - isPreconfigured: false, - referencedByCount: 0, - }, - { - id: '456', - actionTypeId: '.jira', - name: 'Connector without isCaseOwned', - config: { - incidentConfiguration: { - mapping: [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, - apiUrl: 'https://elastic.jira.com', - }, - isPreconfigured: false, - referencedByCount: 0, - }, - ]); + + const expected = getActions(); + expected.shift(); + expect(res.payload).toEqual(expected); }); it('it throws an error when actions client is null', async () => { @@ -109,6 +55,7 @@ describe('GET connectors', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }) ); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts index 547e67379ad6c..cb88f04a9b835 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts @@ -17,39 +17,16 @@ import { RESILIENT_ACTION_TYPE_ID, } from '../../../../../common/constants'; -/** - * We need to take into account connectors that have been created within cases and - * they do not have the isCaseOwned field. Checking for the existence of - * the mapping attribute ensures that the connector is indeed a case connector. - * Cases connector should always have a mapping. - */ - -interface CaseAction extends FindActionResult { - config?: { - isCaseOwned?: boolean; - incidentConfiguration?: Record; - }; -} - -const isCaseOwned = (action: CaseAction): boolean => { - if ( - [SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes( - action.actionTypeId - ) - ) { - if (action.config?.isCaseOwned === true || action.config?.incidentConfiguration?.mapping) { - return true; - } - } - - return false; -}; +const isConnectorSupported = (action: FindActionResult): boolean => + [SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes( + action.actionTypeId + ); /* * Be aware that this api will only return 20 connectors */ -export function initCaseConfigureGetActionConnector({ caseService, router }: RouteDeps) { +export function initCaseConfigureGetActionConnector({ router }: RouteDeps) { router.get( { path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, @@ -63,7 +40,7 @@ export function initCaseConfigureGetActionConnector({ caseService, router }: Rou throw Boom.notFound('Action client have not been found'); } - const results = (await actionsClient.getAll()).filter(isCaseOwned); + const results = (await actionsClient.getAll()).filter(isConnectorSupported); return response.ok({ body: results }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_fields.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_fields.ts new file mode 100644 index 0000000000000..c9b8e671b7df8 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_fields.ts @@ -0,0 +1,70 @@ +/* + * 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 Boom from '@hapi/boom'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { RouteDeps } from '../../types'; +import { escapeHatch, wrapError } from '../../utils'; + +import { CASE_CONFIGURE_CONNECTOR_DETAILS_URL } from '../../../../../common/constants'; +import { + ConnectorRequestParamsRt, + GetFieldsRequestQueryRt, + throwErrors, +} from '../../../../../common/api'; + +export function initCaseConfigureGetFields({ router }: RouteDeps) { + router.get( + { + path: CASE_CONFIGURE_CONNECTOR_DETAILS_URL, + validate: { + params: escapeHatch, + query: escapeHatch, + }, + }, + async (context, request, response) => { + try { + if (!context.case) { + throw Boom.badRequest('RouteHandlerContext is not registered for cases'); + } + const query = pipe( + GetFieldsRequestQueryRt.decode(request.query), + fold(throwErrors(Boom.badRequest), identity) + ); + const params = pipe( + ConnectorRequestParamsRt.decode(request.params), + fold(throwErrors(Boom.badRequest), identity) + ); + + const caseClient = context.case.getCaseClient(); + + const connectorType = query.connector_type; + if (connectorType == null) { + throw Boom.illegal('no connectorType value provided'); + } + + const actionsClient = await context.actions?.getActionsClient(); + if (actionsClient == null) { + throw Boom.notFound('Action client have not been found'); + } + + const res = await caseClient.getFields({ + actionsClient, + connectorId: params.connector_id, + connectorType, + }); + + return response.ok({ + body: res.fields, + }); + } catch (error) { + return response.customError(wrapError(error)); + } + } + ); +} diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/mock.ts b/x-pack/plugins/case/server/routes/api/cases/configure/mock.ts new file mode 100644 index 0000000000000..ed8b208864611 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/cases/configure/mock.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { + ServiceConnectorCaseParams, + ServiceConnectorCommentParams, + ConnectorMappingsAttributes, +} from '../../../../../common/api/connectors'; +export const updateUser = { + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { fullName: 'Another User', username: 'another' }, +}; +const entity = { + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, +}; +export const comment: ServiceConnectorCommentParams = { + comment: 'first comment', + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + ...entity, +}; +export const defaultPipes = ['informationCreated']; +export const params = { + comments: [comment], + description: 'a description', + impact: '3', + savedObjectId: '1231231231232', + severity: '1', + title: 'a title', + urgency: '2', + ...entity, +} as ServiceConnectorCaseParams; +export const mappings: ConnectorMappingsAttributes[] = [ + { + source: 'title', + target: 'short_description', + action_type: 'overwrite', + }, + { + source: 'description', + target: 'description', + action_type: 'append', + }, + { + source: 'comments', + target: 'comments', + action_type: 'append', + }, +]; diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts index 261cd3e6b0884..fd213a514f339 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts @@ -11,6 +11,7 @@ import { createMockSavedObjectsRepository, createRoute, createRouteContext, + mockCaseMappings, } from '../../__fixtures__'; import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; @@ -42,6 +43,7 @@ describe('PATCH configuration', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }) ); @@ -75,6 +77,7 @@ describe('PATCH configuration', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }) ); @@ -113,6 +116,7 @@ describe('PATCH configuration', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }) ); @@ -166,6 +170,7 @@ describe('PATCH configuration', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }) ); @@ -193,6 +198,7 @@ describe('PATCH configuration', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }) ); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts index b3305a2c0b8e4..08db2b3103422 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts @@ -13,6 +13,7 @@ import { CasesConfigurePatchRt, CaseConfigureResponseRt, throwErrors, + ConnectorMappingsAttributes, } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError, escapeHatch } from '../../utils'; @@ -56,6 +57,24 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout const { username, full_name, email } = await caseService.getUser({ request, response }); const updateDate = new Date().toISOString(); + + let mappings: ConnectorMappingsAttributes[] = []; + if (connector != null) { + if (!context.case) { + throw Boom.badRequest('RouteHandlerContext is not registered for cases'); + } + const caseClient = context.case.getCaseClient(); + const actionsClient = await context.actions?.getActionsClient(); + if (actionsClient == null) { + throw Boom.notFound('Action client have not been found'); + } + mappings = await caseClient.getMappings({ + actionsClient, + caseClient, + connectorId: connector.id, + connectorType: connector.type, + }); + } const patch = await caseConfigureService.patch({ client, caseConfigureId: myCaseConfigure.saved_objects[0].id, @@ -68,7 +87,6 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout updated_by: { email, full_name, username }, }, }); - return response.ok({ body: CaseConfigureResponseRt.encode({ ...myCaseConfigure.saved_objects[0].attributes, @@ -76,6 +94,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout connector: transformESConnectorToCaseConnector( patch.attributes.connector ?? myCaseConfigure.saved_objects[0].attributes.connector ), + mappings, version: patch.version ?? '', }), }); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts index 7ef3bdb4a700a..5a5836f595eee 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts @@ -11,9 +11,10 @@ import { createMockSavedObjectsRepository, createRoute, createRouteContext, + mockCaseConfigure, + mockCaseMappings, } from '../../__fixtures__'; -import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; import { initPostCaseConfigure } from './post_configure'; import { newConfiguration } from '../../__mocks__/request_responses'; import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; @@ -40,6 +41,7 @@ describe('POST configuration', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }) ); @@ -75,6 +77,7 @@ describe('POST configuration', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }) ); @@ -115,6 +118,7 @@ describe('POST configuration', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }) ); @@ -140,6 +144,7 @@ describe('POST configuration', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }) ); @@ -165,6 +170,7 @@ describe('POST configuration', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }) ); @@ -190,6 +196,7 @@ describe('POST configuration', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }) ); @@ -215,6 +222,7 @@ describe('POST configuration', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }) ); @@ -232,6 +240,7 @@ describe('POST configuration', () => { const savedObjectRepository = createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }); const context = await createRouteContext(savedObjectRepository); @@ -251,6 +260,7 @@ describe('POST configuration', () => { const savedObjectRepository = createMockSavedObjectsRepository({ caseConfigureSavedObject: [], + caseMappingsSavedObject: mockCaseMappings, }); const context = await createRouteContext(savedObjectRepository); @@ -273,6 +283,7 @@ describe('POST configuration', () => { mockCaseConfigure[0], { ...mockCaseConfigure[0], id: 'mock-configuration-2' }, ], + caseMappingsSavedObject: mockCaseMappings, }); const context = await createRouteContext(savedObjectRepository); @@ -337,6 +348,7 @@ describe('POST configuration', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }) ); @@ -363,6 +375,7 @@ describe('POST configuration', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }) ); @@ -388,6 +401,7 @@ describe('POST configuration', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }) ); @@ -409,6 +423,7 @@ describe('POST configuration', () => { const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: mockCaseMappings, }) ); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts index 97856c84d60fc..8ae4e1211f5f1 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts @@ -32,6 +32,14 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route }, async (context, request, response) => { try { + if (!context.case) { + throw Boom.badRequest('RouteHandlerContext is not registered for cases'); + } + const caseClient = context.case.getCaseClient(); + const actionsClient = await context.actions?.getActionsClient(); + if (actionsClient == null) { + throw Boom.notFound('Action client have not been found'); + } const client = context.core.savedObjects.client; const query = pipe( CasesConfigureRequestRt.decode(request.body), @@ -39,7 +47,6 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route ); const myCaseConfigure = await caseConfigureService.find({ client }); - if (myCaseConfigure.saved_objects.length > 0) { await Promise.all( myCaseConfigure.saved_objects.map((cc) => @@ -51,6 +58,12 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route const { email, full_name, username } = await caseService.getUser({ request, response }); const creationDate = new Date().toISOString(); + const mappings = await caseClient.getMappings({ + actionsClient, + caseClient, + connectorId: query.connector.id, + connectorType: query.connector.type, + }); const post = await caseConfigureService.post({ client, attributes: { @@ -68,6 +81,7 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route ...post.attributes, // Reserve for future implementations connector: transformESConnectorToCaseConnector(post.attributes.connector), + mappings, version: post.version ?? '', }), }); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.ts new file mode 100644 index 0000000000000..9c4c06c5f4e18 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.ts @@ -0,0 +1,80 @@ +/* + * 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 { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import Boom from '@hapi/boom'; +import { RouteDeps } from '../../types'; +import { escapeHatch, wrapError } from '../../utils'; + +import { CASE_CONFIGURE_PUSH_URL } from '../../../../../common/constants'; +import { + ConnectorRequestParamsRt, + PostPushRequestRt, + throwErrors, +} from '../../../../../common/api'; +import { mapIncident } from './utils'; + +export function initPostPushToService({ router, connectorMappingsService }: RouteDeps) { + router.post( + { + path: CASE_CONFIGURE_PUSH_URL, + validate: { + params: escapeHatch, + body: escapeHatch, + }, + }, + async (context, request, response) => { + try { + if (!context.case) { + throw Boom.badRequest('RouteHandlerContext is not registered for cases'); + } + const caseClient = context.case.getCaseClient(); + const actionsClient = await context.actions?.getActionsClient(); + if (actionsClient == null) { + throw Boom.notFound('Action client have not been found'); + } + const params = pipe( + ConnectorRequestParamsRt.decode(request.params), + fold(throwErrors(Boom.badRequest), identity) + ); + const body = pipe( + PostPushRequestRt.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + + const myConnectorMappings = await caseClient.getMappings({ + actionsClient, + caseClient, + connectorId: params.connector_id, + connectorType: body.connector_type, + }); + + const res = await mapIncident( + actionsClient, + params.connector_id, + body.connector_type, + myConnectorMappings, + body.params + ); + const pushRes = await actionsClient.execute({ + actionId: params.connector_id, + params: { + subAction: 'pushToService', + subActionParams: res, + }, + }); + + return response.ok({ + body: pushRes, + }); + } catch (error) { + return response.customError(wrapError(error)); + } + } + ); +} diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/utils.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/utils.test.ts new file mode 100644 index 0000000000000..d2ecdf61c882d --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/cases/configure/utils.test.ts @@ -0,0 +1,385 @@ +/* + * 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 { + prepareFieldsForTransformation, + transformFields, + transformComments, + transformers, +} from './utils'; + +import { comment as commentObj, defaultPipes, mappings, params, updateUser } from './mock'; +import { + ServiceConnectorCaseParams, + ExternalServiceParams, + Incident, +} from '../../../../../common/api/connectors'; +const formatComment = { commentId: commentObj.commentId, comment: commentObj.comment }; +describe('api/cases/configure/utils', () => { + describe('prepareFieldsForTransformation', () => { + test('prepare fields with defaults', () => { + const res = prepareFieldsForTransformation({ + defaultPipes, + params, + mappings, + }); + expect(res).toEqual([ + { + actionType: 'overwrite', + key: 'short_description', + pipes: ['informationCreated'], + value: 'a title', + }, + { + actionType: 'append', + key: 'description', + pipes: ['informationCreated', 'append'], + value: 'a description', + }, + ]); + }); + + test('prepare fields with default pipes', () => { + const res = prepareFieldsForTransformation({ + defaultPipes: ['myTestPipe'], + mappings, + params, + }); + expect(res).toEqual([ + { + actionType: 'overwrite', + key: 'short_description', + pipes: ['myTestPipe'], + value: 'a title', + }, + { + actionType: 'append', + key: 'description', + pipes: ['myTestPipe', 'append'], + value: 'a description', + }, + ]); + }); + }); + describe('transformFields', () => { + test('transform fields for creation correctly', () => { + const fields = prepareFieldsForTransformation({ + defaultPipes, + mappings, + params, + }); + + const res = transformFields({ + params, + fields, + }); + + expect(res).toEqual({ + short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + }); + + test('transform fields for update correctly', () => { + const fields = prepareFieldsForTransformation({ + params, + mappings, + defaultPipes: ['informationUpdated'], + }); + + const res = transformFields({ + params: { + ...params, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { + username: 'anotherUser', + fullName: 'Another User', + }, + }, + fields, + currentIncident: { + short_description: 'first title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + expect(res).toEqual({ + short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)', + description: + 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User) \r\na description (updated at 2020-03-15T08:34:53.450Z by Another User)', + }); + }); + + test('add newline character to description', () => { + const fields = prepareFieldsForTransformation({ + params, + mappings, + defaultPipes: ['informationUpdated'], + }); + + const res = transformFields({ + params, + fields, + currentIncident: { + short_description: 'first title', + description: 'first description', + }, + }); + expect(res.description?.includes('\r\n')).toBe(true); + }); + + test('append username if fullname is undefined when create', () => { + const fields = prepareFieldsForTransformation({ + defaultPipes, + mappings, + params, + }); + + const res = transformFields({ + params: { + ...params, + createdBy: { fullName: '', username: 'elastic' }, + }, + fields, + }); + + expect(res).toEqual({ + short_description: 'a title (created at 2020-03-13T08:34:53.450Z by elastic)', + description: 'a description (created at 2020-03-13T08:34:53.450Z by elastic)', + }); + }); + + test('append username if fullname is undefined when update', () => { + const fields = prepareFieldsForTransformation({ + defaultPipes: ['informationUpdated'], + mappings, + params, + }); + + const res = transformFields({ + params: { + ...params, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { username: 'anotherUser', fullName: '' }, + }, + fields, + }); + + expect(res).toEqual({ + short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by anotherUser)', + description: 'a description (updated at 2020-03-15T08:34:53.450Z by anotherUser)', + }); + }); + }); + describe('transformComments', () => { + test('transform creation comments', () => { + const comments = [commentObj]; + const res = transformComments(comments, ['informationCreated']); + expect(res).toEqual([ + { + ...formatComment, + comment: `${formatComment.comment} (created at ${comments[0].createdAt} by ${comments[0].createdBy.fullName})`, + }, + ]); + }); + + test('transform update comments', () => { + const comments = [ + { + ...commentObj, + ...updateUser, + }, + ]; + const res = transformComments(comments, ['informationUpdated']); + expect(res).toEqual([ + { + ...formatComment, + comment: `${formatComment.comment} (updated at ${updateUser.updatedAt} by ${updateUser.updatedBy.fullName})`, + }, + ]); + }); + + test('transform added comments', () => { + const comments = [commentObj]; + const res = transformComments(comments, ['informationAdded']); + expect(res).toEqual([ + { + ...formatComment, + comment: `${formatComment.comment} (added at ${comments[0].createdAt} by ${comments[0].createdBy.fullName})`, + }, + ]); + }); + + test('transform comments without fullname', () => { + const comments = [{ ...commentObj, createdBy: { username: commentObj.createdBy.username } }]; + // @ts-ignore testing no fullName + const res = transformComments(comments, ['informationAdded']); + expect(res).toEqual([ + { + ...formatComment, + comment: `${formatComment.comment} (added at ${comments[0].createdAt} by ${comments[0].createdBy.username})`, + }, + ]); + }); + + test('adds update user correctly', () => { + const comments = [ + { + ...commentObj, + updatedAt: '2020-04-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic2', username: 'elastic' }, + }, + ]; + const res = transformComments(comments, ['informationAdded']); + expect(res).toEqual([ + { + ...formatComment, + comment: `${formatComment.comment} (added at ${comments[0].updatedAt} by ${comments[0].updatedBy.fullName})`, + }, + ]); + }); + + test('adds update user with empty fullname correctly', () => { + const comments = [ + { + ...commentObj, + updatedAt: '2020-04-13T08:34:53.450Z', + updatedBy: { fullName: '', username: 'elastic2' }, + }, + ]; + const res = transformComments(comments, ['informationAdded']); + expect(res).toEqual([ + { + ...formatComment, + comment: `${formatComment.comment} (added at ${comments[0].updatedAt} by ${comments[0].updatedBy.username})`, + }, + ]); + }); + }); + describe('transformers', () => { + const { informationCreated, informationUpdated, informationAdded, append } = transformers; + describe('informationCreated', () => { + test('transforms correctly', () => { + const res = informationCreated({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + }); + expect(res).toEqual({ value: 'a value (created at 2020-04-15T08:19:27.400Z by elastic)' }); + }); + + test('transforms correctly without optional fields', () => { + const res = informationCreated({ + value: 'a value', + }); + expect(res).toEqual({ value: 'a value (created at by )' }); + }); + + test('returns correctly rest fields', () => { + const res = informationCreated({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + previousValue: 'previous value', + }); + expect(res).toEqual({ + value: 'a value (created at 2020-04-15T08:19:27.400Z by elastic)', + previousValue: 'previous value', + }); + }); + }); + + describe('informationUpdated', () => { + test('transforms correctly', () => { + const res = informationUpdated({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + }); + expect(res).toEqual({ value: 'a value (updated at 2020-04-15T08:19:27.400Z by elastic)' }); + }); + + test('transforms correctly without optional fields', () => { + const res = informationUpdated({ + value: 'a value', + }); + expect(res).toEqual({ value: 'a value (updated at by )' }); + }); + + test('returns correctly rest fields', () => { + const res = informationUpdated({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + previousValue: 'previous value', + }); + expect(res).toEqual({ + value: 'a value (updated at 2020-04-15T08:19:27.400Z by elastic)', + previousValue: 'previous value', + }); + }); + }); + + describe('informationAdded', () => { + test('transforms correctly', () => { + const res = informationAdded({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + }); + expect(res).toEqual({ value: 'a value (added at 2020-04-15T08:19:27.400Z by elastic)' }); + }); + + test('transforms correctly without optional fields', () => { + const res = informationAdded({ + value: 'a value', + }); + expect(res).toEqual({ value: 'a value (added at by )' }); + }); + + test('returns correctly rest fields', () => { + const res = informationAdded({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + previousValue: 'previous value', + }); + expect(res).toEqual({ + value: 'a value (added at 2020-04-15T08:19:27.400Z by elastic)', + previousValue: 'previous value', + }); + }); + }); + + describe('append', () => { + test('transforms correctly', () => { + const res = append({ + value: 'a value', + previousValue: 'previous value', + }); + expect(res).toEqual({ value: 'previous value \r\na value' }); + }); + + test('transforms correctly without optional fields', () => { + const res = append({ + value: 'a value', + }); + expect(res).toEqual({ value: 'a value' }); + }); + + test('returns correctly rest fields', () => { + const res = append({ + value: 'a value', + user: 'elastic', + previousValue: 'previous value', + }); + expect(res).toEqual({ + value: 'previous value \r\na value', + user: 'elastic', + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/utils.ts b/x-pack/plugins/case/server/routes/api/cases/configure/utils.ts new file mode 100644 index 0000000000000..b8a37661fe9f7 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/cases/configure/utils.ts @@ -0,0 +1,237 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { flow } from 'lodash'; +import { + ServiceConnectorCaseParams, + ServiceConnectorCommentParams, + ConnectorMappingsAttributes, + ConnectorTypes, + EntityInformation, + ExternalServiceParams, + Incident, + JiraPushToServiceApiParams, + MapIncident, + PipedField, + PrepareFieldsForTransformArgs, + PushToServiceApiParams, + ResilientPushToServiceApiParams, + ServiceNowPushToServiceApiParams, + SimpleComment, + Transformer, + TransformerArgs, + TransformFieldsArgs, +} from '../../../../../common/api'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ActionsClient } from '../../../../../../actions/server/actions_client'; + +export const mapIncident = async ( + actionsClient: ActionsClient, + connectorId: string, + connectorType: string, + mappings: ConnectorMappingsAttributes[], + params: ServiceConnectorCaseParams +): Promise => { + const { comments: caseComments, externalId } = params; + const defaultPipes = externalId ? ['informationUpdated'] : ['informationCreated']; + let currentIncident: ExternalServiceParams | undefined; + const service = serviceFormatter(connectorType, params); + if (service == null) { + throw new Error(`Invalid service`); + } + const thirdPartyName = service.thirdPartyName; + let incident: Partial = service.incident; + if (externalId) { + try { + currentIncident = ((await actionsClient.execute({ + actionId: connectorId, + params: { + subAction: 'getIncident', + subActionParams: { externalId }, + }, + })) as unknown) as ExternalServiceParams | undefined; + } catch (ex) { + throw new Error( + `Retrieving Incident by id ${externalId} from ${thirdPartyName} failed with exception: ${ex}` + ); + } + } + + const fields = prepareFieldsForTransformation({ + defaultPipes, + mappings, + params, + }); + + const transformedFields = transformFields< + ServiceConnectorCaseParams, + ExternalServiceParams, + Incident + >({ + params, + fields, + currentIncident, + }); + incident = { ...incident, ...transformedFields, externalId }; + let comments: SimpleComment[] = []; + if (caseComments && Array.isArray(caseComments) && caseComments.length > 0) { + const commentsMapping = mappings.find((m) => m.source === 'comments'); + if (commentsMapping?.action_type !== 'nothing') { + comments = transformComments(caseComments, ['informationAdded']); + } + } + return { incident, comments }; +}; + +export const serviceFormatter = ( + connectorType: string, + params: unknown +): { thirdPartyName: string; incident: Partial } | null => { + switch (connectorType) { + case ConnectorTypes.jira: + const { + priority, + labels, + issueType, + parent, + } = params as JiraPushToServiceApiParams['incident']; + return { + incident: { priority, labels, issueType, parent }, + thirdPartyName: 'Jira', + }; + case ConnectorTypes.resilient: + const { incidentTypes, severityCode } = params as ResilientPushToServiceApiParams['incident']; + return { + incident: { incidentTypes, severityCode }, + thirdPartyName: 'Resilient', + }; + case ConnectorTypes.servicenow: + const { severity, urgency, impact } = params as ServiceNowPushToServiceApiParams['incident']; + return { + incident: { severity, urgency, impact }, + thirdPartyName: 'ServiceNow', + }; + default: + return null; + } +}; + +export const getEntity = (entity: EntityInformation): string => + (entity.updatedBy != null + ? entity.updatedBy.fullName + ? entity.updatedBy.fullName + : entity.updatedBy.username + : entity.createdBy != null + ? entity.createdBy.fullName + ? entity.createdBy.fullName + : entity.createdBy.username + : '') ?? ''; + +export const FIELD_INFORMATION = ( + mode: string, + date: string | undefined, + user: string | undefined +) => { + switch (mode) { + case 'create': + return i18n.translate('xpack.case.connectors.case.externalIncidentCreated', { + values: { date, user }, + defaultMessage: '(created at {date} by {user})', + }); + case 'update': + return i18n.translate('xpack.case.connectors.case.externalIncidentUpdated', { + values: { date, user }, + defaultMessage: '(updated at {date} by {user})', + }); + case 'add': + return i18n.translate('xpack.case.connectors.case.externalIncidentAdded', { + values: { date, user }, + defaultMessage: '(added at {date} by {user})', + }); + default: + return i18n.translate('xpack.case.connectors.case.externalIncidentDefault', { + values: { date, user }, + defaultMessage: '(created at {date} by {user})', + }); + } +}; +export const transformers: Record = { + informationCreated: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({ + value: `${value} ${FIELD_INFORMATION('create', date, user)}`, + ...rest, + }), + informationUpdated: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({ + value: `${value} ${FIELD_INFORMATION('update', date, user)}`, + ...rest, + }), + informationAdded: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({ + value: `${value} ${FIELD_INFORMATION('add', date, user)}`, + ...rest, + }), + append: ({ value, previousValue, ...rest }: TransformerArgs): TransformerArgs => ({ + value: previousValue ? `${previousValue} \r\n${value}` : `${value}`, + ...rest, + }), +}; +export const prepareFieldsForTransformation = ({ + defaultPipes, + mappings, + params, +}: PrepareFieldsForTransformArgs): PipedField[] => + mappings.reduce( + (acc: PipedField[], mapping) => + mapping != null && + mapping.target !== 'not_mapped' && + mapping.action_type !== 'nothing' && + mapping.source !== 'comments' + ? [ + ...acc, + { + key: mapping.target, + value: params[mapping.source] ?? '', + actionType: mapping.action_type, + pipes: mapping.action_type === 'append' ? [...defaultPipes, 'append'] : defaultPipes, + }, + ] + : acc, + [] + ); + +export const transformFields = < + P extends EntityInformation, + S extends Record, + R extends {} +>({ + params, + fields, + currentIncident, +}: TransformFieldsArgs): R => { + return fields.reduce((prev, cur) => { + const transform = flow(...cur.pipes.map((p) => transformers[p])); + return { + ...prev, + [cur.key]: transform({ + value: cur.value, + date: params.updatedAt ?? params.createdAt, + user: getEntity(params), + previousValue: currentIncident ? currentIncident[cur.key] : '', + }).value, + }; + }, {} as R); +}; + +export const transformComments = ( + comments: ServiceConnectorCommentParams[], + pipes: string[] +): SimpleComment[] => + comments.map((c) => ({ + comment: flow(...pipes.map((p) => transformers[p]))({ + value: c.comment, + date: c.updatedAt ?? c.createdAt, + user: getEntity(c), + }).value, + commentId: c.commentId, + })); diff --git a/x-pack/plugins/case/server/routes/api/index.ts b/x-pack/plugins/case/server/routes/api/index.ts index ced88fabf3160..587e43b218f44 100644 --- a/x-pack/plugins/case/server/routes/api/index.ts +++ b/x-pack/plugins/case/server/routes/api/index.ts @@ -25,8 +25,10 @@ import { initPostCommentApi } from './cases/comments/post_comment'; import { initCaseConfigureGetActionConnector } from './cases/configure/get_connectors'; import { initGetCaseConfigure } from './cases/configure/get_configure'; +import { initCaseConfigureGetFields } from './cases/configure/get_fields'; import { initPatchCaseConfigure } from './cases/configure/patch_configure'; import { initPostCaseConfigure } from './cases/configure/post_configure'; +import { initPostPushToService } from './cases/configure/post_push_to_service'; import { RouteDeps } from './types'; @@ -52,6 +54,8 @@ export function initCaseApi(deps: RouteDeps) { initGetCaseConfigure(deps); initPatchCaseConfigure(deps); initPostCaseConfigure(deps); + initCaseConfigureGetFields(deps); + initPostPushToService(deps); // Reporters initGetReportersApi(deps); // Status diff --git a/x-pack/plugins/case/server/routes/api/types.ts b/x-pack/plugins/case/server/routes/api/types.ts index e532a7b618b5c..0b93d844fe9ab 100644 --- a/x-pack/plugins/case/server/routes/api/types.ts +++ b/x-pack/plugins/case/server/routes/api/types.ts @@ -9,13 +9,15 @@ import { CaseConfigureServiceSetup, CaseServiceSetup, CaseUserActionServiceSetup, + ConnectorMappingsServiceSetup, } from '../../services'; export interface RouteDeps { caseConfigureService: CaseConfigureServiceSetup; caseService: CaseServiceSetup; - userActionService: CaseUserActionServiceSetup; + connectorMappingsService: ConnectorMappingsServiceSetup; router: IRouter; + userActionService: CaseUserActionServiceSetup; } export enum SortFieldCase { diff --git a/x-pack/plugins/case/server/saved_object_types/connector_mappings.ts b/x-pack/plugins/case/server/saved_object_types/connector_mappings.ts new file mode 100644 index 0000000000000..b928d8b5c577c --- /dev/null +++ b/x-pack/plugins/case/server/saved_object_types/connector_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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsType } from 'src/core/server'; + +export const CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT = 'cases-connector-mappings'; + +export const caseConnectorMappingsSavedObjectType: SavedObjectsType = { + name: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, + hidden: false, + namespaceType: 'single', + mappings: { + properties: { + mappings: { + properties: { + source: { + type: 'keyword', + }, + target: { + type: 'keyword', + }, + action_type: { + type: 'keyword', + }, + }, + }, + }, + }, +}; diff --git a/x-pack/plugins/case/server/saved_object_types/index.ts b/x-pack/plugins/case/server/saved_object_types/index.ts index 0e4b9fa3e2eee..36d38cad797b6 100644 --- a/x-pack/plugins/case/server/saved_object_types/index.ts +++ b/x-pack/plugins/case/server/saved_object_types/index.ts @@ -8,3 +8,7 @@ export { caseSavedObjectType, CASE_SAVED_OBJECT } from './cases'; export { caseConfigureSavedObjectType, CASE_CONFIGURE_SAVED_OBJECT } from './configure'; export { caseCommentSavedObjectType, CASE_COMMENT_SAVED_OBJECT } from './comments'; export { caseUserActionSavedObjectType, CASE_USER_ACTION_SAVED_OBJECT } from './user_actions'; +export { + caseConnectorMappingsSavedObjectType, + CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, +} from './connector_mappings'; diff --git a/x-pack/plugins/case/server/services/connector_mappings/index.ts b/x-pack/plugins/case/server/services/connector_mappings/index.ts new file mode 100644 index 0000000000000..32afbebfa6215 --- /dev/null +++ b/x-pack/plugins/case/server/services/connector_mappings/index.ts @@ -0,0 +1,59 @@ +/* + * 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 { + Logger, + SavedObject, + SavedObjectReference, + SavedObjectsClientContract, + SavedObjectsFindResponse, +} from 'kibana/server'; + +import { ConnectorMappings, SavedObjectFindOptions } from '../../../common/api'; +import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../saved_object_types'; + +interface ClientArgs { + client: SavedObjectsClientContract; +} +interface FindConnectorMappingsArgs extends ClientArgs { + options?: SavedObjectFindOptions; +} + +interface PostConnectorMappingsArgs extends ClientArgs { + attributes: ConnectorMappings; + references: SavedObjectReference[]; +} + +export interface ConnectorMappingsServiceSetup { + find(args: FindConnectorMappingsArgs): Promise>; + post(args: PostConnectorMappingsArgs): Promise>; +} + +export class ConnectorMappingsService { + constructor(private readonly log: Logger) {} + public setup = async (): Promise => ({ + find: async ({ client, options }: FindConnectorMappingsArgs) => { + try { + this.log.debug(`Attempting to find all connector mappings`); + return await client.find({ ...options, type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT }); + } catch (error) { + this.log.debug(`Attempting to find all connector mappings`); + throw error; + } + }, + post: async ({ client, attributes, references }: PostConnectorMappingsArgs) => { + try { + this.log.debug(`Attempting to POST a new connector mappings`); + return await client.create(CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, attributes, { + references, + }); + } catch (error) { + this.log.debug(`Error on POST a new connector mappings: ${error}`); + throw error; + } + }, + }); +} diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index 95bcf87361e07..e75b597fa7af2 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -31,6 +31,7 @@ import { readTags } from './tags/read_tags'; export { CaseConfigureService, CaseConfigureServiceSetup } from './configure'; export { CaseUserActionService, CaseUserActionServiceSetup } from './user_actions'; +export { ConnectorMappingsService, ConnectorMappingsServiceSetup } from './connector_mappings'; export { AlertService, AlertServiceContract } from './alerts'; export interface ClientArgs { diff --git a/x-pack/plugins/case/server/services/mocks.ts b/x-pack/plugins/case/server/services/mocks.ts index 01a8cb09ac2d5..65f2c845bb400 100644 --- a/x-pack/plugins/case/server/services/mocks.ts +++ b/x-pack/plugins/case/server/services/mocks.ts @@ -5,14 +5,16 @@ */ import { + AlertServiceContract, CaseConfigureServiceSetup, CaseServiceSetup, CaseUserActionServiceSetup, - AlertServiceContract, + ConnectorMappingsServiceSetup, } from '.'; export type CaseServiceMock = jest.Mocked; export type CaseConfigureServiceMock = jest.Mocked; +export type ConnectorMappingsServiceMock = jest.Mocked; export type CaseUserActionServiceMock = jest.Mocked; export type AlertServiceMock = jest.Mocked; @@ -43,6 +45,11 @@ export const createConfigureServiceMock = (): CaseConfigureServiceMock => ({ post: jest.fn(), }); +export const connectorMappingsServiceMock = (): ConnectorMappingsServiceMock => ({ + find: jest.fn(), + post: jest.fn(), +}); + export const createUserActionServiceMock = (): CaseUserActionServiceMock => ({ getUserActions: jest.fn(), postUserActions: jest.fn(), diff --git a/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts index f664c61df298a..acb56d1f24668 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts @@ -5,7 +5,7 @@ */ import { serviceNowConnector } from '../objects/case'; -import { TOASTER } from '../screens/configure_cases'; +import { SERVICE_NOW_MAPPING, TOASTER } from '../screens/configure_cases'; import { goToEditExternalConnection } from '../tasks/all_cases'; import { @@ -18,9 +18,33 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { CASES_URL } from '../urls/navigation'; describe('Cases connectors', () => { + const configureResult = { + connector: { + id: 'e271c3b8-f702-4fbc-98e0-db942b573bbd', + name: 'SN', + type: '.servicenow', + fields: null, + }, + closure_type: 'close-by-user', + created_at: '2020-12-01T16:28:09.219Z', + created_by: { email: null, full_name: null, username: 'elastic' }, + updated_at: null, + updated_by: null, + mappings: [ + { source: 'title', target: 'short_description', action_type: 'overwrite' }, + { source: 'description', target: 'description', action_type: 'overwrite' }, + { source: 'comments', target: 'comments', action_type: 'append' }, + ], + version: 'WzEwNCwxXQ==', + }; before(() => { cy.intercept('POST', '/api/actions/action').as('createConnector'); - cy.intercept('POST', '/api/cases/configure').as('saveConnector'); + cy.intercept('POST', '/api/cases/configure', (req) => { + const connector = req.body.connector; + req.reply((res) => { + res.send(200, { ...configureResult, connector }); + }); + }).as('saveConnector'); }); it('Configures a new connector', () => { @@ -37,6 +61,7 @@ describe('Cases connectors', () => { selectLastConnectorCreated(response!.body.id); cy.wait('@saveConnector', { timeout: 10000 }).its('response.statusCode').should('eql', 200); + cy.get(SERVICE_NOW_MAPPING).first().should('have.text', 'short_description'); cy.get(TOASTER).should('have.text', 'Saved external connection settings'); }); }); diff --git a/x-pack/plugins/security_solution/cypress/objects/case.ts b/x-pack/plugins/security_solution/cypress/objects/case.ts index 64d331dc66e38..b7a2b21b63402 100644 --- a/x-pack/plugins/security_solution/cypress/objects/case.ts +++ b/x-pack/plugins/security_solution/cypress/objects/case.ts @@ -83,14 +83,6 @@ export const mockConnectorsResponse = [ actionTypeId: '.jira', name: 'Jira', config: { - incidentConfiguration: { - mapping: [ - { source: 'title', target: 'summary', actionType: 'overwrite' }, - { source: 'description', target: 'description', actionType: 'overwrite' }, - { source: 'comments', target: 'comments', actionType: 'append' }, - ], - }, - isCaseOwned: true, apiUrl: 'https://siem-kibana.atlassian.net', projectKey: 'RJ', }, @@ -102,14 +94,6 @@ export const mockConnectorsResponse = [ actionTypeId: '.resilient', name: 'Resilient', config: { - incidentConfiguration: { - mapping: [ - { source: 'title', target: 'name', actionType: 'overwrite' }, - { source: 'description', target: 'description', actionType: 'overwrite' }, - { source: 'comments', target: 'comments', actionType: 'append' }, - ], - }, - isCaseOwned: true, apiUrl: 'https://ibm-resilient.siem.estc.dev', orgId: '201', }, @@ -121,14 +105,6 @@ export const mockConnectorsResponse = [ actionTypeId: '.servicenow', name: 'ServiceNow', config: { - incidentConfiguration: { - mapping: [ - { source: 'title', target: 'short_description', actionType: 'overwrite' }, - { source: 'description', target: 'description', actionType: 'overwrite' }, - { source: 'comments', target: 'comments', actionType: 'append' }, - ], - }, - isCaseOwned: true, apiUrl: 'https://dev65287.service-now.com', }, isPreconfigured: false, diff --git a/x-pack/plugins/security_solution/cypress/screens/configure_cases.ts b/x-pack/plugins/security_solution/cypress/screens/configure_cases.ts index 006c524a38acb..72dd78363b6d0 100644 --- a/x-pack/plugins/security_solution/cypress/screens/configure_cases.ts +++ b/x-pack/plugins/security_solution/cypress/screens/configure_cases.ts @@ -28,3 +28,5 @@ export const TOASTER = '[data-test-subj="euiToastHeader"]'; export const URL = '[data-test-subj="apiUrlFromInput"]'; export const USERNAME = '[data-test-subj="connector-servicenow-username-form-input"]'; + +export const SERVICE_NOW_MAPPING = 'code[data-test-subj="field-mapping-target"]'; diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx index f3b0b2b840ab4..b1b5f2b087eee 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx @@ -8,9 +8,8 @@ import { ActionConnector } from '../../../containers/configure/types'; import { UseConnectorsResponse } from '../../../containers/configure/use_connectors'; import { connectorsMock } from '../../../containers/configure/mock'; import { ReturnUseCaseConfigure } from '../../../containers/configure/use_configure'; -export { mapping } from '../../../containers/configure/mock'; import { ConnectorTypes } from '../../../../../../case/common/api'; - +export { mappings } from '../../../containers/configure/mock'; export const connectors: ActionConnector[] = connectorsMock; // x - pack / plugins / triggers_actions_ui; @@ -36,14 +35,14 @@ export const useCaseConfigureResponse: ReturnUseCaseConfigure = { }, firstLoad: false, loading: false, - mapping: null, + mappings: [], persistCaseConfigure: jest.fn(), persistLoading: false, refetchCaseConfigure: jest.fn(), setClosureType: jest.fn(), setConnector: jest.fn(), setCurrentConfiguration: jest.fn(), - setMapping: jest.fn(), + setMappings: jest.fn(), version: '', }; diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.test.tsx index 5272d13043fc4..149775215df60 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.test.tsx @@ -11,6 +11,7 @@ import { Connectors, Props } from './connectors'; import { TestProviders } from '../../../common/mock'; import { ConnectorsDropdown } from './connectors_dropdown'; import { connectors } from './__mock__'; +import { ConnectorTypes } from '../../../../../case/common/api/connectors'; describe('Connectors', () => { let wrapper: ReactWrapper; @@ -18,13 +19,14 @@ describe('Connectors', () => { const handleShowEditFlyout = jest.fn(); const props: Props = { - disabled: false, - updateConnectorDisabled: false, connectors, - selectedConnector: 'none', + disabled: false, + handleShowEditFlyout, isLoading: false, + mappings: [], onChangeConnector, - handleShowEditFlyout, + selectedConnector: { id: 'none', type: ConnectorTypes.none }, + updateConnectorDisabled: false, }; beforeAll(() => { @@ -66,9 +68,15 @@ describe('Connectors', () => { test('the connector is changed successfully to none', () => { onChangeConnector.mockClear(); - const newWrapper = mount(, { - wrappingComponent: TestProviders, - }); + const newWrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); newWrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); newWrapper.find('button[data-test-subj="dropdown-connector-no-connector"]').simulate('click'); @@ -87,9 +95,15 @@ describe('Connectors', () => { }); test('the text of the update button is shown correctly', () => { - const newWrapper = mount(, { - wrappingComponent: TestProviders, - }); + const newWrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); expect( newWrapper diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.tsx index fd7510a1c4713..f937796496fc7 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.tsx @@ -18,7 +18,9 @@ import styled from 'styled-components'; import { ConnectorsDropdown } from './connectors_dropdown'; import * as i18n from './translations'; -import { ActionConnector } from '../../containers/configure/types'; +import { ActionConnector, CaseConnectorMapping } from '../../containers/configure/types'; +import { Mapping } from './mapping'; +import { ConnectorTypes } from '../../../../../case/common/api/connectors'; const EuiFormRowExtended = styled(EuiFormRow)` .euiFormRow__labelWrapper { @@ -31,24 +33,26 @@ const EuiFormRowExtended = styled(EuiFormRow)` export interface Props { connectors: ActionConnector[]; disabled: boolean; + handleShowEditFlyout: () => void; isLoading: boolean; - updateConnectorDisabled: boolean; + mappings: CaseConnectorMapping[]; onChangeConnector: (id: string) => void; - selectedConnector: string; - handleShowEditFlyout: () => void; + selectedConnector: { id: string; type: string }; + updateConnectorDisabled: boolean; } const ConnectorsComponent: React.FC = ({ connectors, - isLoading, disabled, - updateConnectorDisabled, + handleShowEditFlyout, + isLoading, + mappings, onChangeConnector, selectedConnector, - handleShowEditFlyout, + updateConnectorDisabled, }) => { const connectorsName = useMemo( - () => connectors.find((c) => c.id === selectedConnector)?.name ?? 'none', - [connectors, selectedConnector] + () => connectors.find((c) => c.id === selectedConnector.id)?.name ?? 'none', + [connectors, selectedConnector.id] ); const dropDownLabel = useMemo( @@ -68,10 +72,8 @@ const ConnectorsComponent: React.FC = ({ ), - // eslint-disable-next-line react-hooks/exhaustive-deps - [connectorsName, updateConnectorDisabled] + [connectorsName, handleShowEditFlyout, updateConnectorDisabled] ); - return ( <> = ({ label={dropDownLabel} data-test-subj="case-connectors-form-row" > - + + + + + {selectedConnector.type !== ConnectorTypes.none ? ( + + + + ) : null} + diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.test.tsx index e79c031c7002c..937946fa14253 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.test.tsx @@ -7,77 +7,48 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; -import { connectorsConfiguration, createDefaultMapping } from '../connectors'; - import { FieldMapping, FieldMappingProps } from './field_mapping'; -import { mapping } from './__mock__'; -import { FieldMappingRow } from './field_mapping_row'; +import { mappings } from './__mock__'; import { TestProviders } from '../../../common/mock'; +import { FieldMappingRowStatic } from './field_mapping_row_static'; describe('FieldMappingRow', () => { let wrapper: ReactWrapper; - const onChangeMapping = jest.fn(); const props: FieldMappingProps = { - disabled: false, - mapping, - onChangeMapping, + isLoading: false, + mappings, connectorActionTypeId: '.servicenow', }; beforeAll(() => { wrapper = mount(, { wrappingComponent: TestProviders }); }); - test('it renders', () => { expect( - wrapper.find('[data-test-subj="case-configure-field-mapping-cols"]').first().exists() - ).toBe(true); - - expect( - wrapper.find('[data-test-subj="case-configure-field-mapping-row-wrapper"]').first().exists() + wrapper.find('[data-test-subj="case-configure-field-mappings-row-wrapper"]').first().exists() ).toBe(true); - expect(wrapper.find(FieldMappingRow).length).toEqual(3); + expect(wrapper.find(FieldMappingRowStatic).length).toEqual(3); }); - test('it shows the correct number of FieldMappingRow with default mapping', () => { - const newWrapper = mount(, { + test('it does not render without mappings', () => { + const newWrapper = mount(, { wrappingComponent: TestProviders, }); - - expect(newWrapper.find(FieldMappingRow).length).toEqual(3); + expect( + newWrapper + .find('[data-test-subj="case-configure-field-mappings-row-wrapper"]') + .first() + .exists() + ).toBe(false); }); test('it pass the corrects props to mapping row', () => { - const rows = wrapper.find(FieldMappingRow); - rows.forEach((row, index) => { - expect(row.prop('securitySolutionField')).toEqual(mapping[index].source); - expect(row.prop('selectedActionType')).toEqual(mapping[index].actionType); - expect(row.prop('selectedThirdParty')).toEqual(mapping[index].target); - }); - }); - - test('it pass the default mapping when mapping is null', () => { - const newWrapper = mount(, { - wrappingComponent: TestProviders, - }); - - const selectedConnector = connectorsConfiguration['.servicenow']; - const defaultMapping = createDefaultMapping(selectedConnector.fields); - - const rows = newWrapper.find(FieldMappingRow); + const rows = wrapper.find(FieldMappingRowStatic); rows.forEach((row, index) => { - expect(row.prop('securitySolutionField')).toEqual(defaultMapping[index].source); - expect(row.prop('selectedActionType')).toEqual(defaultMapping[index].actionType); - expect(row.prop('selectedThirdParty')).toEqual(defaultMapping[index].target); + expect(row.prop('securitySolutionField')).toEqual(mappings[index].source); + expect(row.prop('selectedActionType')).toEqual(mappings[index].actionType); + expect(row.prop('selectedThirdParty')).toEqual(mappings[index].target); }); }); - - test('it should show zero rows on empty array', () => { - const newWrapper = mount(, { - wrappingComponent: TestProviders, - }); - - expect(newWrapper.find(FieldMappingRow).length).toEqual(0); - }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.tsx index b15c82f254aea..e930c00b8e173 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping.tsx @@ -4,148 +4,69 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useMemo } from 'react'; -import { EuiFormRow, EuiFlexItem, EuiFlexGroup, EuiSuperSelectOption } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import styled from 'styled-components'; -import { - CasesConfigurationMapping, - CaseField, - ActionType, - ThirdPartyField, -} from '../../containers/configure/types'; -import { - ThirdPartyField as ConnectorConfigurationThirdPartyField, - AllThirdPartyFields, - createDefaultMapping, - connectorsConfiguration, -} from '../connectors'; - -import { FieldMappingRow } from './field_mapping_row'; +import { FieldMappingRowStatic } from './field_mapping_row_static'; import * as i18n from './translations'; -import { setActionTypeToMapping, setThirdPartyToMapping } from './utils'; + +import { CaseConnectorMapping } from '../../containers/configure/types'; +import { connectorsConfiguration } from '../connectors'; const FieldRowWrapper = styled.div` - margin-top: 8px; + margin: 10px 0; font-size: 14px; `; -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 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; + isLoading: boolean; + mappings: CaseConnectorMapping[]; } const FieldMappingComponent: React.FC = ({ - disabled, - mapping, - onChangeMapping, connectorActionTypeId, + isLoading, + mappings, }) => { - const onChangeActionType = useCallback( - (caseField: CaseField, newActionType: ActionType) => { - const myMapping = mapping ?? defaultMapping; - onChangeMapping(setActionTypeToMapping(caseField, newActionType, myMapping)); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [mapping] + const selectedConnector = useMemo( + () => connectorsConfiguration[connectorActionTypeId] ?? { fields: {} }, + [connectorActionTypeId] ); - - const onChangeThirdParty = useCallback( - (caseField: CaseField, newThirdPartyField: ThirdPartyField) => { - const myMapping = mapping ?? defaultMapping; - onChangeMapping(setThirdPartyToMapping(caseField, newThirdPartyField, myMapping)); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [mapping] - ); - - const selectedConnector = connectorsConfiguration[connectorActionTypeId] ?? { fields: {} }; - const defaultMapping = useMemo(() => createDefaultMapping(selectedConnector.fields), [ - selectedConnector.fields, - ]); - - return ( - <> - + return mappings.length ? ( + + + {' '} {i18n.FIELD_MAPPING_FIRST_COL} - {i18n.FIELD_MAPPING_SECOND_COL} + + {i18n.FIELD_MAPPING_SECOND_COL(selectedConnector.name)} + {i18n.FIELD_MAPPING_THIRD_COL} - - - {(mapping ?? defaultMapping).map((item) => ( - - ))} - - - ); + + + + {mappings.map((item) => ( + + ))} + + + + ) : null; }; export const FieldMapping = React.memo(FieldMappingComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.test.tsx deleted file mode 100644 index a2acd0e20b6ad..0000000000000 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.test.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { mount, ReactWrapper } from 'enzyme'; -import { EuiSuperSelectOption, EuiSuperSelect } from '@elastic/eui'; - -import { FieldMappingRow, RowProps } from './field_mapping_row'; -import { TestProviders } from '../../../common/mock'; -import { ThirdPartyField, ActionType } from '../../containers/configure/types'; - -const thirdPartyOptions: Array> = [ - { - value: 'short_description', - inputDisplay: {'Short Description'}, - 'data-test-subj': 'third-party-short-desc', - }, - { - value: 'description', - inputDisplay: {'Description'}, - 'data-test-subj': 'third-party-desc', - }, -]; - -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, - securitySolutionField: 'title', - thirdPartyOptions, - actionTypeOptions, - onChangeActionType, - onChangeThirdParty, - selectedActionType: 'nothing', - selectedThirdParty: 'short_description', - }; - - beforeAll(() => { - wrapper = mount(, { wrappingComponent: TestProviders }); - }); - - test('it renders', () => { - expect( - wrapper.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-title"]').first().exists() - ).toBe(true); - }); - - test('it passes thirdPartyOptions correctly', () => { - const selectProps = wrapper.find(EuiSuperSelect).first().props(); - - expect(selectProps.options).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - value: 'short_description', - 'data-test-subj': 'third-party-short-desc', - }), - expect.objectContaining({ - value: 'description', - 'data-test-subj': 'third-party-desc', - }), - ]) - ); - }); - - test('it passes the correct actionTypeOptions', () => { - const selectProps = wrapper.find(EuiSuperSelect).at(1).props(); - - expect(selectProps.options).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - value: 'nothing', - 'data-test-subj': 'edit-update-option-nothing', - }), - expect.objectContaining({ - value: 'overwrite', - 'data-test-subj': 'edit-update-option-overwrite', - }), - expect.objectContaining({ - value: 'append', - 'data-test-subj': 'edit-update-option-append', - }), - ]) - ); - }); -}); diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.tsx deleted file mode 100644 index b924cad1475a8..0000000000000 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useMemo } from 'react'; -import { - EuiFlexItem, - EuiFlexGroup, - EuiSuperSelect, - EuiIcon, - EuiSuperSelectOption, -} from '@elastic/eui'; - -import { capitalize } from 'lodash/fp'; -import { CaseField, ActionType, ThirdPartyField } from '../../containers/configure/types'; -import { AllThirdPartyFields } from '../connectors'; - -export interface RowProps { - id: string; - disabled: boolean; - securitySolutionField: CaseField; - thirdPartyOptions: Array>; - actionTypeOptions: Array>; - onChangeActionType: (caseField: CaseField, newActionType: ActionType) => void; - onChangeThirdParty: (caseField: CaseField, newThirdPartyField: ThirdPartyField) => void; - selectedActionType: ActionType; - selectedThirdParty: ThirdPartyField; -} - -const FieldMappingRowComponent: React.FC = ({ - id, - disabled, - securitySolutionField, - thirdPartyOptions, - actionTypeOptions, - onChangeActionType, - onChangeThirdParty, - selectedActionType, - selectedThirdParty, -}) => { - const securitySolutionFieldCapitalized = useMemo(() => capitalize(securitySolutionField), [ - securitySolutionField, - ]); - return ( - - - - - {securitySolutionFieldCapitalized} - - - - - - - - - - - - - - ); -}; - -export const FieldMappingRow = React.memo(FieldMappingRowComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row_static.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row_static.tsx new file mode 100644 index 0000000000000..e68ee3d69a7db --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/field_mapping_row_static.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { EuiCode, EuiFlexItem, EuiFlexGroup, EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; + +import { capitalize } from 'lodash/fp'; +import { CaseField, ActionType, ThirdPartyField } from '../../containers/configure/types'; + +export interface RowProps { + isLoading: boolean; + securitySolutionField: CaseField; + selectedActionType: ActionType; + selectedThirdParty: ThirdPartyField; +} + +const FieldMappingRowComponent: React.FC = ({ + isLoading, + securitySolutionField, + selectedActionType, + selectedThirdParty, +}) => { + const selectedActionTypeCapitalized = useMemo(() => capitalize(selectedActionType), [ + selectedActionType, + ]); + return ( + + + + + {securitySolutionField} + + + + + + + + + + {isLoading ? ( + + ) : ( + {selectedThirdParty} + )} + + + + + {isLoading ? : selectedActionTypeCapitalized} + + + ); +}; + +export const FieldMappingRowStatic = React.memo(FieldMappingRowComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx index 5150a907ae712..2656e2496c2fc 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx @@ -169,7 +169,7 @@ describe('ConfigureCases', () => { beforeEach(() => { useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping: connectors[0].config.incidentConfiguration.mapping, + mappings: [], closureType: 'close-by-user', connector: { id: 'servicenow-1', @@ -198,7 +198,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('servicenow-1'); + expect(wrapper.find(Connectors).prop('selectedConnector').id).toBe('servicenow-1'); // ClosureOptions expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(false); @@ -247,7 +247,7 @@ describe('ConfigureCases', () => { beforeEach(() => { useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping: connectors[1].config.incidentConfiguration.mapping, + mapping: null, closureType: 'close-by-user', connector: { id: 'resilient-2', @@ -374,7 +374,7 @@ describe('ConfigureCases', () => { persistCaseConfigure = jest.fn(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping: connectors[0].config.incidentConfiguration.mapping, + mapping: null, closureType: 'close-by-user', connector: { id: 'resilient-2', @@ -462,7 +462,7 @@ describe('closure options', () => { persistCaseConfigure = jest.fn(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping: connectors[0].config.incidentConfiguration.mapping, + mapping: null, closureType: 'close-by-user', connector: { id: 'servicenow-1', @@ -508,7 +508,7 @@ describe('user interactions', () => { beforeEach(() => { useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping: connectors[1].config.incidentConfiguration.mapping, + mapping: null, closureType: 'close-by-user', connector: { id: 'resilient-2', diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx index e2dd405b7804d..6176b679c3a03 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx @@ -69,6 +69,7 @@ const ConfigureCasesComponent: React.FC = ({ userC connector, closureType, loading: loadingCaseConfigure, + mappings, persistLoading, persistCaseConfigure, setConnector, @@ -83,7 +84,6 @@ const ConfigureCasesComponent: React.FC = ({ userC const reloadConnectors = useCallback(async () => refetchConnectors(), []); const isLoadingAny = isLoadingConnectors || persistLoading || loadingCaseConfigure; const updateConnectorDisabled = isLoadingAny || !connectorIsValid || connector.id === 'none'; - const onClickUpdateConnector = useCallback(() => { setEditFlyoutVisibility(true); }, []); @@ -201,11 +201,12 @@ const ConfigureCasesComponent: React.FC = ({ userC {addFlyoutVisible && ConnectorAddFlyout} diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.test.tsx index 0bbee42a4f2ef..5c31f4256f888 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.test.tsx @@ -9,19 +9,15 @@ import { mount, ReactWrapper } from 'enzyme'; import { TestProviders } from '../../../common/mock'; import { Mapping, MappingProps } from './mapping'; -import { mapping } from './__mock__'; +import { mappings } from './__mock__'; describe('Mapping', () => { let wrapper: ReactWrapper; - const onChangeMapping = jest.fn(); const setEditFlyoutVisibility = jest.fn(); const props: MappingProps = { - disabled: false, - mapping, - updateConnectorDisabled: false, - onChangeMapping, - setEditFlyoutVisibility, connectorActionTypeId: '.servicenow', + isLoading: false, + mappings, }; beforeEach(() => { @@ -32,186 +28,33 @@ describe('Mapping', () => { afterEach(() => { wrapper.unmount(); }); - describe('Common', () => { test('it shows mapping form group', () => { - expect(wrapper.find('[data-test-subj="case-mapping-form-group"]').first().exists()).toBe( - true - ); + expect(wrapper.find('[data-test-subj="static-mappings"]').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('correctly maps fields', () => { + expect(wrapper.find('[data-test-subj="field-mapping-source"] code').first().text()).toBe( + 'title' + ); + expect(wrapper.find('[data-test-subj="field-mapping-target"] code').first().text()).toBe( + 'short_description' + ); }); - - test('it shows the update button', () => { + // skipping until next PR + test.skip('it shows the update button', () => { expect( - wrapper.find('[data-test-subj="case-mapping-update-connector-button"]').first().exists() + wrapper.find('[data-test-subj="case-mappings-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('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(); - - 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', () => { + test.skip('it triggers update flyout', () => { + expect(setEditFlyoutVisibility).not.toHaveBeenCalled(); wrapper - .find('button[data-test-subj="case-configure-action-type-select-title"]') + .find('button[data-test-subj="case-mappings-update-connector-button"]') + .first() .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 correct action types', () => { - wrapper - .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(); - }); + expect(setEditFlyoutVisibility).toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.tsx index 2c3172a30f159..7d3456a3df819 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.tsx @@ -4,72 +4,49 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import styled from 'styled-components'; +import React, { useMemo } from 'react'; -import { - EuiDescribedFormGroup, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiButtonEmpty, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTextColor } from '@elastic/eui'; import * as i18n from './translations'; import { FieldMapping } from './field_mapping'; -import { CasesConfigurationMapping } from '../../containers/configure/types'; +import { CaseConnectorMapping } from '../../containers/configure/types'; +import { connectorsConfiguration } from '../connectors'; export interface MappingProps { - disabled: boolean; - updateConnectorDisabled: boolean; - mapping: CasesConfigurationMapping[] | null; connectorActionTypeId: string; - onChangeMapping: (newMapping: CasesConfigurationMapping[]) => void; - setEditFlyoutVisibility: () => void; + isLoading: boolean; + mappings: CaseConnectorMapping[]; } -const EuiButtonEmptyExtended = styled(EuiButtonEmpty)` - font-size: 12px; - height: 24px; -`; - const MappingComponent: React.FC = ({ - disabled, - updateConnectorDisabled, - mapping, - onChangeMapping, - setEditFlyoutVisibility, connectorActionTypeId, + isLoading, + mappings, }) => { + const selectedConnector = useMemo(() => connectorsConfiguration[connectorActionTypeId], [ + connectorActionTypeId, + ]); return ( - {i18n.FIELD_MAPPING_TITLE}} - description={i18n.FIELD_MAPPING_DESC} - data-test-subj="case-mapping-form-group" - > - - - - - {i18n.UPDATE_CONNECTOR} - - - - - - + + + +

{i18n.FIELD_MAPPING_TITLE(selectedConnector.name)}

+ + {i18n.FIELD_MAPPING_DESC(selectedConnector.name)} + +
+
+ + + +
); }; diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/translations.ts b/x-pack/plugins/security_solution/public/cases/components/configure_cases/translations.ts index a6ae082c4721a..6586b23dde18c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/translations.ts @@ -80,21 +80,26 @@ export const CASE_CLOSURE_OPTIONS_CLOSED_INCIDENT = i18n.translate( defaultMessage: 'Automatically close Security cases when incident is closed in external system', } ); +export const FIELD_MAPPING_TITLE = (thirdPartyName: string): string => { + return i18n.translate('xpack.securitySolution.case.configureCases.fieldMappingTitle', { + values: { thirdPartyName }, + defaultMessage: '{ thirdPartyName } field mappings', + }); +}; -export const FIELD_MAPPING_TITLE = i18n.translate( - 'xpack.securitySolution.case.configureCases.fieldMappingTitle', - { - defaultMessage: 'Field mappings', - } -); - -export const FIELD_MAPPING_DESC = i18n.translate( - 'xpack.securitySolution.case.configureCases.fieldMappingDesc', - { +export const FIELD_MAPPING_DESC = (thirdPartyName: string): string => { + return i18n.translate('xpack.securitySolution.case.configureCases.fieldMappingDesc', { + values: { thirdPartyName }, defaultMessage: - 'Map Security case fields when pushing data to a third-party. Field mappings require an established connection to an external incident management system.', - } -); + 'Map Security Case fields to { thirdPartyName } fields when pushing data to { thirdPartyName }. Field mappings require an established connection to { thirdPartyName }.', + }); +}; +export const EDIT_FIELD_MAPPING_TITLE = (thirdPartyName: string): string => { + return i18n.translate('xpack.securitySolution.case.configureCases.editFieldMappingTitle', { + values: { thirdPartyName }, + defaultMessage: 'Edit { thirdPartyName } field mappings', + }); +}; export const FIELD_MAPPING_FIRST_COL = i18n.translate( 'xpack.securitySolution.case.configureCases.fieldMappingFirstCol', @@ -103,12 +108,12 @@ export const FIELD_MAPPING_FIRST_COL = i18n.translate( } ); -export const FIELD_MAPPING_SECOND_COL = i18n.translate( - 'xpack.securitySolution.case.configureCases.fieldMappingSecondCol', - { - defaultMessage: 'External incident field', - } -); +export const FIELD_MAPPING_SECOND_COL = (thirdPartyName: string): string => { + return i18n.translate('xpack.securitySolution.case.configureCases.fieldMappingSecondCol', { + values: { thirdPartyName }, + defaultMessage: '{ thirdPartyName } field', + }); +}; export const FIELD_MAPPING_THIRD_COL = i18n.translate( 'xpack.securitySolution.case.configureCases.fieldMappingThirdCol', @@ -142,6 +147,17 @@ export const CANCEL = i18n.translate('xpack.securitySolution.case.configureCases defaultMessage: 'Cancel', }); +export const SAVE = i18n.translate('xpack.securitySolution.case.configureCases.saveButton', { + defaultMessage: 'Save', +}); + +export const SAVE_CLOSE = i18n.translate( + 'xpack.securitySolution.case.configureCases.saveAndCloseButton', + { + defaultMessage: 'Save & Close', + } +); + export const WARNING_NO_CONNECTOR_TITLE = i18n.translate( 'xpack.securitySolution.case.configureCases.warningTitle', { @@ -164,10 +180,36 @@ export const MAPPING_FIELD_NOT_MAPPED = i18n.translate( } ); -export const UPDATE_CONNECTOR = i18n.translate( +export const COMMENT = i18n.translate('xpack.securitySolution.case.configureCases.commentMapping', { + defaultMessage: 'Comments', +}); + +export const NO_FIELDS_ERROR = (connectorName: string): string => { + return i18n.translate('xpack.securitySolution.case.configureCases.noFieldsError', { + values: { connectorName }, + defaultMessage: + 'No { connectorName } fields found. Please check your { connectorName } connector settings or your { connectorName } instance settings to resolve.', + }); +}; + +export const BLANK_MAPPINGS = (connectorName: string): string => { + return i18n.translate('xpack.securitySolution.case.configureCases.blankMappings', { + values: { connectorName }, + defaultMessage: 'At least one field needs to be mapped to { connectorName }', + }); +}; + +export const REQUIRED_MAPPINGS = (connectorName: string, fields: string): string => { + return i18n.translate('xpack.securitySolution.case.configureCases.requiredMappings', { + values: { connectorName, fields }, + defaultMessage: + 'At least one Case field needs to be mapped to the following required { connectorName } fields: { fields }', + }); +}; +export const UPDATE_FIELD_MAPPINGS = i18n.translate( 'xpack.securitySolution.case.configureCases.updateConnector', { - defaultMessage: 'Update connector', + defaultMessage: 'Update field mappings', } ); diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/utils.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/utils.test.tsx index d6755f687100f..5e9b86429e9c6 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/utils.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/utils.test.tsx @@ -4,35 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mapping } from './__mock__'; +import { mappings } from './__mock__'; import { setActionTypeToMapping, setThirdPartyToMapping } from './utils'; -import { CasesConfigurationMapping } from '../../containers/configure/types'; +import { CaseConnectorMapping } from '../../containers/configure/types'; describe('FieldMappingRow', () => { test('it should change the action type', () => { - const newMapping = setActionTypeToMapping('title', 'nothing', mapping); + const newMapping = setActionTypeToMapping('title', 'nothing', mappings); expect(newMapping[0].actionType).toBe('nothing'); }); test('it should not change other fields', () => { - const [newTitle, description, comments] = setActionTypeToMapping('title', 'nothing', mapping); - expect(newTitle).not.toEqual(mapping[0]); - expect(description).toEqual(mapping[1]); - expect(comments).toEqual(mapping[2]); + const [newTitle, description, comments] = setActionTypeToMapping('title', 'nothing', mappings); + expect(newTitle).not.toEqual(mappings[0]); + expect(description).toEqual(mappings[1]); + expect(comments).toEqual(mappings[2]); }); test('it should return a new array when changing action type', () => { - const newMapping = setActionTypeToMapping('title', 'nothing', mapping); - expect(newMapping).not.toBe(mapping); + const newMapping = setActionTypeToMapping('title', 'nothing', mappings); + expect(newMapping).not.toBe(mappings); }); test('it should change the third party', () => { - const newMapping = setThirdPartyToMapping('title', 'description', mapping); + const newMapping = setThirdPartyToMapping('title', 'description', mappings); expect(newMapping[0].target).toBe('description'); }); test('it should not change other fields when there is not a conflict', () => { - const tempMapping: CasesConfigurationMapping[] = [ + const tempMapping: CaseConnectorMapping[] = [ { source: 'title', target: 'short_description', @@ -47,17 +47,17 @@ describe('FieldMappingRow', () => { const [newTitle, comments] = setThirdPartyToMapping('title', 'description', tempMapping); - expect(newTitle).not.toEqual(mapping[0]); + expect(newTitle).not.toEqual(mappings[0]); expect(comments).toEqual(tempMapping[1]); }); test('it should return a new array when changing third party', () => { - const newMapping = setThirdPartyToMapping('title', 'description', mapping); - expect(newMapping).not.toBe(mapping); + const newMapping = setThirdPartyToMapping('title', 'description', mappings); + expect(newMapping).not.toBe(mappings); }); test('it should change the target of the conflicting third party field to not_mapped', () => { - const newMapping = setThirdPartyToMapping('title', 'description', mapping); + const newMapping = setThirdPartyToMapping('title', 'description', mappings); expect(newMapping[1].target).toBe('not_mapped'); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/utils.ts b/x-pack/plugins/security_solution/public/cases/components/configure_cases/utils.ts index 11691b5dac332..cacfc30a5cdaf 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/utils.ts +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/utils.ts @@ -7,17 +7,17 @@ import { ConnectorTypeFields, ConnectorTypes } from '../../../../../case/common/ import { CaseField, ActionType, - CasesConfigurationMapping, ThirdPartyField, ActionConnector, CaseConnector, + CaseConnectorMapping, } from '../../containers/configure/types'; export const setActionTypeToMapping = ( caseField: CaseField, newActionType: ActionType, - mapping: CasesConfigurationMapping[] -): CasesConfigurationMapping[] => { + mapping: CaseConnectorMapping[] +): CaseConnectorMapping[] => { const findItemIndex = mapping.findIndex((item) => item.source === caseField); if (findItemIndex >= 0) { @@ -34,8 +34,8 @@ export const setActionTypeToMapping = ( export const setThirdPartyToMapping = ( caseField: CaseField, newThirdPartyField: ThirdPartyField, - mapping: CasesConfigurationMapping[] -): CasesConfigurationMapping[] => + mapping: CaseConnectorMapping[] +): CaseConnectorMapping[] => mapping.map((item) => { if (item.source !== caseField && item.target === newThirdPartyField) { return { ...item, target: 'not_mapped' }; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts index e77aa9bdd84b1..e08c0c9771366 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts @@ -8,4 +8,3 @@ export { getActionType as getCaseConnectorUI } from './case'; export * from './config'; export * from './types'; -export * from './utils'; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/utils.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/utils.ts deleted file mode 100644 index 0a6dd37d9f9e2..0000000000000 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/utils.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CasesConfigurationMapping } from '../../../cases/containers/configure/types'; - -import { ThirdPartyField } from './types'; - -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/security_solution/public/cases/components/settings/jira/api.ts b/x-pack/plugins/security_solution/public/cases/components/settings/jira/api.ts index 5aaa3fc38b102..dbda7199f6ccf 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/jira/api.ts +++ b/x-pack/plugins/security_solution/public/cases/components/settings/jira/api.ts @@ -5,7 +5,7 @@ */ import { HttpSetup } from 'kibana/public'; -import { ActionTypeExecutorResult } from '../../../../../../case/common/api'; +import { ActionTypeExecutorResult } from '../../../../../../actions/common'; import { IssueTypes, Fields, Issues, Issue } from './types'; export const BASE_ACTION_API_PATH = '/api/actions'; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/api.ts b/x-pack/plugins/security_solution/public/cases/components/settings/resilient/api.ts index 961df85226ebf..57f26afe54e27 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/api.ts +++ b/x-pack/plugins/security_solution/public/cases/components/settings/resilient/api.ts @@ -5,7 +5,7 @@ */ import { HttpSetup } from 'kibana/public'; -import { ActionTypeExecutorResult } from '../../../../../../case/common/api'; +import { ActionTypeExecutorResult } from '../../../../../../actions/common'; import { ResilientIncidentTypes, ResilientSeverity } from './types'; export const BASE_ACTION_API_PATH = '/api/actions'; diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx index bec1ab3dd4292..9c23081bac535 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx @@ -52,6 +52,7 @@ import { import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; import * as i18n from './translations'; +import { getCaseConfigurePushUrl } from '../../../../case/common/api/helpers'; const abortCtrl = new AbortController(); const mockKibanaServices = KibanaServices.get as jest.Mock; @@ -454,18 +455,24 @@ describe('Case Configuration API', () => { }); const connectorId = 'connectorId'; test('check url, method, signal', async () => { - await pushToService(connectorId, casePushParams, abortCtrl.signal); - expect(fetchMock).toHaveBeenCalledWith(`/api/actions/action/${connectorId}/_execute`, { + await pushToService(connectorId, ConnectorTypes.jira, casePushParams, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${getCaseConfigurePushUrl(connectorId)}`, { method: 'POST', body: JSON.stringify({ - params: { subAction: 'pushToService', subActionParams: casePushParams }, + connector_type: ConnectorTypes.jira, + params: casePushParams, }), signal: abortCtrl.signal, }); }); test('happy path', async () => { - const resp = await pushToService(connectorId, casePushParams, abortCtrl.signal); + const resp = await pushToService( + connectorId, + ConnectorTypes.jira, + casePushParams, + abortCtrl.signal + ); expect(resp).toEqual(serviceConnector); }); @@ -478,7 +485,7 @@ describe('Case Configuration API', () => { message: 'not it', }); await expect( - pushToService(connectorId, casePushParams, abortCtrl.signal) + pushToService(connectorId, ConnectorTypes.jira, casePushParams, abortCtrl.signal) ).rejects.toMatchObject({ message: theError }); }); @@ -490,7 +497,7 @@ describe('Case Configuration API', () => { message: theError, }); await expect( - pushToService(connectorId, casePushParams, abortCtrl.signal) + pushToService(connectorId, ConnectorTypes.jira, casePushParams, abortCtrl.signal) ).rejects.toMatchObject({ message: theError }); }); @@ -501,7 +508,7 @@ describe('Case Configuration API', () => { status: 'error', }); await expect( - pushToService(connectorId, casePushParams, abortCtrl.signal) + pushToService(connectorId, ConnectorTypes.jira, casePushParams, abortCtrl.signal) ).rejects.toMatchObject({ message: theError }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.ts b/x-pack/plugins/security_solution/public/cases/containers/api.ts index 5bfa071804713..ef1e35b8ceb4b 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/api.ts @@ -5,36 +5,37 @@ */ import { - CaseResponse, - CasesResponse, - CasesFindResponse, + CaseExternalServiceRequest, CasePatchRequest, CasePostRequest, + CaseResponse, + CasesFindResponse, + CasesResponse, CasesStatusResponse, - CommentRequest, - User, + CaseStatuses, CaseUserActionsResponse, - CaseExternalServiceRequest, + CommentRequest, + CommentType, + ConnectorField, ServiceConnectorCaseParams, ServiceConnectorCaseResponse, - ActionTypeExecutorResult, - CommentType, - CaseStatuses, + User, } from '../../../../case/common/api'; import { + ACTION_TYPES_URL, + CASE_CONFIGURE_CONNECTORS_URL, + CASE_REPORTERS_URL, CASE_STATUS_URL, - CASES_URL, CASE_TAGS_URL, - CASE_REPORTERS_URL, - ACTION_TYPES_URL, - ACTION_URL, + CASES_URL, } from '../../../../case/common/constants'; import { + getCaseCommentsUrl, + getCaseConfigurePushUrl, getCaseDetailsUrl, getCaseUserActionUrl, - getCaseCommentsUrl, } from '../../../../case/common/api/helpers'; import { KibanaServices } from '../../common/lib/kibana'; @@ -61,9 +62,8 @@ import { decodeCaseUserActionsResponse, decodeServiceConnectorCaseResponse, } from './utils'; - import * as i18n from './translations'; - +import { ActionTypeExecutorResult } from '../../../../actions/common'; export const getCase = async ( caseId: string, includeComments: boolean = true, @@ -245,15 +245,17 @@ export const pushCase = async ( export const pushToService = async ( connectorId: string, + connectorType: string, casePushParams: ServiceConnectorCaseParams, signal: AbortSignal ): Promise => { const response = await KibanaServices.get().http.fetch< ActionTypeExecutorResult> - >(`${ACTION_URL}/action/${connectorId}/_execute`, { + >(`${getCaseConfigurePushUrl(connectorId)}`, { method: 'POST', body: JSON.stringify({ - params: { subAction: 'pushToService', subActionParams: casePushParams }, + connector_type: connectorType, + params: casePushParams, }), signal, }); @@ -261,7 +263,6 @@ export const pushToService = async ( if (response.status === 'error') { throw new Error(response.serviceMessage ?? response.message ?? i18n.ERROR_PUSH_TO_SERVICE); } - return decodeServiceConnectorCaseResponse(response.data); }; @@ -272,3 +273,20 @@ export const getActionLicense = async (signal: AbortSignal): Promise => { + const response = await KibanaServices.get().http.fetch( + `${CASE_CONFIGURE_CONNECTORS_URL}/${connectorId}`, + { + query: { + connector_type: connectorType, + }, + method: 'GET', + signal, + } + ); + return response; +}; diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts index 647bc1b466674..8652e48fd834d 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts @@ -47,6 +47,15 @@ export const getCaseConfigure = async ({ signal }: ApiProps): Promise => { + const response = await KibanaServices.get().http.fetch(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`, { + method: 'GET', + signal, + }); + + return response; +}; + export const postCaseConfigure = async ( caseConfiguration: CasesConfigureRequest, signal: AbortSignal diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts index 83c9e6fa71c24..589760be92ab3 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts @@ -10,9 +10,9 @@ import { CasesConfigureRequest, ConnectorTypes, } from '../../../../../case/common/api'; -import { CaseConfigure, CasesConfigurationMapping } from './types'; +import { CaseConfigure, CaseConnectorMapping } from './types'; -export const mapping: CasesConfigurationMapping[] = [ +export const mappings: CaseConnectorMapping[] = [ { source: 'title', target: 'short_description', @@ -21,7 +21,7 @@ export const mapping: CasesConfigurationMapping[] = [ { source: 'description', target: 'description', - actionType: 'append', + actionType: 'overwrite', }, { source: 'comments', @@ -36,10 +36,6 @@ export const connectorsMock: ActionConnector[] = [ name: 'My Connector', config: { apiUrl: 'https://instance1.service-now.com', - incidentConfiguration: { - mapping, - }, - isCaseOwned: true, }, isPreconfigured: false, }, @@ -50,25 +46,6 @@ export const connectorsMock: ActionConnector[] = [ config: { apiUrl: 'https://test/', orgId: '201', - incidentConfiguration: { - mapping: [ - { - source: 'title', - target: 'name', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, }, isPreconfigured: false, }, @@ -78,25 +55,6 @@ export const connectorsMock: ActionConnector[] = [ name: 'Jira', config: { apiUrl: 'https://instance.atlassian.ne', - incidentConfiguration: { - mapping: [ - { - source: 'title', - target: 'summary', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, }, isPreconfigured: false, }, @@ -112,6 +70,7 @@ export const caseConfigurationResposeMock: CasesConfigureResponse = { fields: null, }, closure_type: 'close-by-pushing', + mappings: [], updated_at: '2020-04-06T14:03:18.657Z', updated_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, version: 'WzHJ12', @@ -137,6 +96,7 @@ export const caseConfigurationCamelCaseResponseMock: CaseConfigure = { fields: null, }, closureType: 'close-by-pushing', + mappings: [], updatedAt: '2020-04-06T14:03:18.657Z', updatedBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, version: 'WzHJ12', diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/types.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/types.ts index 879ed5e7a367a..8ec005212e4e1 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/types.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/types.ts @@ -6,42 +6,30 @@ import { ElasticUser } from '../types'; import { + ActionConnector, ActionType, - CasesConfigurationMaps, + CaseConnector, CaseField, + CasesConfigure, ClosureType, ThirdPartyField, - CasesConfigure, - ActionConnector, - CaseConnector, } from '../../../../../case/common/api'; -export { - ActionType, - CasesConfigurationMaps, - CaseField, - ClosureType, - ThirdPartyField, - ActionConnector, - CaseConnector, -}; +export { ActionConnector, ActionType, CaseConnector, CaseField, ClosureType, ThirdPartyField }; -export interface CasesConfigurationMapping { - source: CaseField; - target: ThirdPartyField; +export interface CaseConnectorMapping { actionType: ActionType; + source: CaseField; + target: string; } export interface CaseConfigure { + closureType: ClosureType; + connector: CasesConfigure['connector']; createdAt: string; createdBy: ElasticUser; - connector: CasesConfigure['connector']; - closureType: ClosureType; + mappings: CaseConnectorMapping[]; updatedAt: string; updatedBy: ElasticUser; version: string; } - -export interface CCMapsCombinedActionAttributes extends CasesConfigurationMaps { - actionType?: ActionType; -} diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.test.tsx index 342cdd8b80284..3dd17190b6199 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.test.tsx @@ -11,7 +11,7 @@ import { ReturnUseCaseConfigure, ConnectorConfiguration, } from './use_configure'; -import { mapping, caseConfigurationCamelCaseResponseMock } from './mock'; +import { mappings, caseConfigurationCamelCaseResponseMock } from './mock'; import * as api from './api'; import { ConnectorTypes } from '../../../../../case/common/api/connectors'; @@ -46,7 +46,7 @@ describe('useConfigure', () => { setCurrentConfiguration: result.current.setCurrentConfiguration, setConnector: result.current.setConnector, setClosureType: result.current.setClosureType, - setMapping: result.current.setMapping, + setMappings: result.current.setMappings, }); }); }); @@ -66,15 +66,16 @@ describe('useConfigure', () => { closureType: caseConfigurationCamelCaseResponseMock.closureType, connector: caseConfigurationCamelCaseResponseMock.connector, }, - version: caseConfigurationCamelCaseResponseMock.version, + mappings: [], firstLoad: true, loading: false, - refetchCaseConfigure: result.current.refetchCaseConfigure, persistCaseConfigure: result.current.persistCaseConfigure, - setCurrentConfiguration: result.current.setCurrentConfiguration, - setConnector: result.current.setConnector, + refetchCaseConfigure: result.current.refetchCaseConfigure, setClosureType: result.current.setClosureType, - setMapping: result.current.setMapping, + setConnector: result.current.setConnector, + setCurrentConfiguration: result.current.setCurrentConfiguration, + setMappings: result.current.setMappings, + version: caseConfigurationCamelCaseResponseMock.version, }); }); }); @@ -100,9 +101,9 @@ describe('useConfigure', () => { ); await waitForNextUpdate(); await waitForNextUpdate(); - expect(result.current.mapping).toEqual(null); - result.current.setMapping(mapping); - expect(result.current.mapping).toEqual(mapping); + expect(result.current.mappings).toEqual([]); + result.current.setMappings(mappings); + expect(result.current.mappings).toEqual(mappings); }); }); @@ -205,13 +206,13 @@ describe('useConfigure', () => { expect(result.current).toEqual({ ...initialState, loading: false, + persistCaseConfigure: result.current.persistCaseConfigure, persistLoading: false, refetchCaseConfigure: result.current.refetchCaseConfigure, - persistCaseConfigure: result.current.persistCaseConfigure, - setCurrentConfiguration: result.current.setCurrentConfiguration, - setConnector: result.current.setConnector, setClosureType: result.current.setClosureType, - setMapping: result.current.setMapping, + setConnector: result.current.setConnector, + setCurrentConfiguration: result.current.setCurrentConfiguration, + setMappings: result.current.setMappings, }); }); }); @@ -249,12 +250,13 @@ describe('useConfigure', () => { }, firstLoad: true, loading: false, - refetchCaseConfigure: result.current.refetchCaseConfigure, + mappings: [], persistCaseConfigure: result.current.persistCaseConfigure, - setCurrentConfiguration: result.current.setCurrentConfiguration, - setConnector: result.current.setConnector, + refetchCaseConfigure: result.current.refetchCaseConfigure, setClosureType: result.current.setClosureType, - setMapping: result.current.setMapping, + setConnector: result.current.setConnector, + setCurrentConfiguration: result.current.setCurrentConfiguration, + setMappings: result.current.setMappings, }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx index 16b813d9f4336..0ed10592dadfb 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx @@ -13,7 +13,7 @@ import { displaySuccessToast, } from '../../../common/components/toasters'; import * as i18n from './translations'; -import { CasesConfigurationMapping, ClosureType, CaseConfigure, CaseConnector } from './types'; +import { ClosureType, CaseConfigure, CaseConnector, CaseConnectorMapping } from './types'; import { ConnectorTypes } from '../../../../../case/common/api/connectors'; export type ConnectorConfiguration = { connector: CaseConnector } & { @@ -24,7 +24,7 @@ export interface State extends ConnectorConfiguration { currentConfiguration: ConnectorConfiguration; firstLoad: boolean; loading: boolean; - mapping: CasesConfigurationMapping[] | null; + mappings: CaseConnectorMapping[]; persistLoading: boolean; version: string; } @@ -58,8 +58,8 @@ export type Action = closureType: ClosureType; } | { - type: 'setMapping'; - mapping: CasesConfigurationMapping[]; + type: 'setMappings'; + mappings: CaseConnectorMapping[]; }; export const configureCasesReducer = (state: State, action: Action) => { @@ -102,10 +102,10 @@ export const configureCasesReducer = (state: State, action: Action) => { closureType: action.closureType, }; } - case 'setMapping': { + case 'setMappings': { return { ...state, - mapping: action.mapping, + mappings: action.mappings, }; } default: @@ -119,7 +119,7 @@ export interface ReturnUseCaseConfigure extends State { setClosureType: (closureType: ClosureType) => void; setConnector: (connector: CaseConnector) => void; setCurrentConfiguration: (configuration: ConnectorConfiguration) => void; - setMapping: (newMapping: CasesConfigurationMapping[]) => void; + setMappings: (newMapping: CaseConnectorMapping[]) => void; } export const initialState: State = { @@ -141,7 +141,7 @@ export const initialState: State = { }, firstLoad: false, loading: true, - mapping: null, + mappings: [], persistLoading: false, version: '', }; @@ -170,10 +170,10 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { }); }, []); - const setMapping = useCallback((newMapping: CasesConfigurationMapping[]) => { + const setMappings = useCallback((mappings: CaseConnectorMapping[]) => { dispatch({ - mapping: newMapping, - type: 'setMapping', + mappings, + type: 'setMappings', }); }, []); @@ -222,6 +222,7 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { setClosureType(res.closureType); } setVersion(res.version); + setMappings(res.mappings); if (!state.firstLoad) { setFirstLoad(true); @@ -285,6 +286,7 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { setClosureType(res.closureType); } setVersion(res.version); + setMappings(res.mappings); if (setCurrentConfiguration != null) { setCurrentConfiguration({ closureType: res.closureType, @@ -299,6 +301,7 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { } } catch (error) { if (!didCancel) { + setConnector(state.currentConfiguration.connector); setPersistLoading(false); errorToToaster({ title: i18n.ERROR_TITLE, @@ -314,8 +317,16 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { abortCtrl.abort(); }; }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [state.version] + [ + dispatchToaster, + setClosureType, + setConnector, + setCurrentConfiguration, + setMappings, + setPersistLoading, + setVersion, + state, + ] ); useEffect(() => { @@ -330,6 +341,6 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { setCurrentConfiguration, setConnector, setClosureType, - setMapping, + setMappings, }; }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/mock.ts index f94fb189c90ce..2b647de2b14ed 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/mock.ts @@ -144,7 +144,6 @@ const basicAction = { }; export const casePushParams = { - actionBy: elasticUser, savedObjectId: basicCaseId, createdAt: basicCreatedAt, createdBy: elasticUser, @@ -156,6 +155,7 @@ export const casePushParams = { description: 'nice', comments: null, }; + export const actionTypeExecutorResult = { actionId: 'string', status: 'ok', diff --git a/x-pack/plugins/security_solution/public/cases/containers/translations.ts b/x-pack/plugins/security_solution/public/cases/containers/translations.ts index 96d2ec2f874db..b0dafcec97cce 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/translations.ts @@ -65,3 +65,10 @@ export const ERROR_PUSH_TO_SERVICE = i18n.translate( defaultMessage: 'Error pushing to service', } ); + +export const ERROR_GET_FIELDS = i18n.translate( + 'xpack.securitySolution.case.configure.errorGetFields', + { + defaultMessage: 'Error getting fields from service', + } +); diff --git a/x-pack/plugins/security_solution/public/cases/containers/types.ts b/x-pack/plugins/security_solution/public/cases/containers/types.ts index a5c9c65dab62a..f83f8c70e5d87 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/types.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/types.ts @@ -132,3 +132,8 @@ export interface DeleteCase { id: string; title?: string; } + +export interface FieldMappings { + id: string; + title?: string; +} diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_fields.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_fields.tsx new file mode 100644 index 0000000000000..6b594fa60e0c7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_fields.tsx @@ -0,0 +1,82 @@ +/* + * 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 { useCallback, useEffect, useState } from 'react'; + +import { errorToToaster, useStateToaster } from '../../common/components/toasters'; +import { getFields } from './api'; +import * as i18n from './translations'; +import { ConnectorField } from '../../../../case/common/api'; + +interface FieldsState { + fields: ConnectorField[]; + isLoading: boolean; + isError: boolean; +} + +const initialData: FieldsState = { + fields: [], + isLoading: false, + isError: false, +}; + +export interface UseGetFields extends FieldsState { + fetchFields: () => void; +} + +export const useGetFields = (connectorId: string, connectorType: string): UseGetFields => { + const [fieldsState, setFieldsState] = useState(initialData); + const [, dispatchToaster] = useStateToaster(); + + const fetchFields = useCallback(() => { + let didCancel = false; + const abortCtrl = new AbortController(); + const fetchData = async () => { + setFieldsState({ + ...fieldsState, + isLoading: true, + }); + try { + const response = await getFields(connectorId, connectorType, abortCtrl.signal); + if (!didCancel) { + setFieldsState({ + fields: response, + isLoading: false, + isError: false, + }); + } + } catch (error) { + if (!didCancel) { + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + setFieldsState({ + fields: [], + isLoading: false, + isError: true, + }); + } + } + }; + fetchData(); + return () => { + didCancel = true; + abortCtrl.abort(); + }; + }, [connectorId, connectorType, dispatchToaster, fieldsState]); + + useEffect(() => { + fetchFields(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return { + ...fieldsState, + fetchFields, + }; +}; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.test.tsx index fe8c793817509..71711dae69319 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.test.tsx @@ -141,6 +141,7 @@ describe('usePostPushToService', () => { await waitForNextUpdate(); expect(spyOnPushToService).toBeCalledWith( samplePush.connector.id, + samplePush.connector.type, formatServiceRequestData( basicCase, samplePush.connector, @@ -174,6 +175,7 @@ describe('usePostPushToService', () => { await waitForNextUpdate(); expect(spyOnPushToService).toBeCalledWith( samplePush2.connector.id, + samplePush2.connector.type, formatServiceRequestData(basicCase, samplePush2.connector, {}), abortCtrl.signal ); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx index d78799d5baafc..97fd0c99ffd96 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx @@ -102,6 +102,7 @@ export const usePostPushToService = (): UsePostPushToService => { const casePushData = await getCase(caseId, true, abortCtrl.signal); const responseService = await pushToService( connector.id, + connector.type, formatServiceRequestData(casePushData, connector, caseServices), abortCtrl.signal ); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7040e94fef984..99e8ed84ba99b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4584,13 +4584,6 @@ "xpack.actions.actionTypeRegistry.get.missingActionTypeErrorMessage": "アクションタイプ \"{id}\" は登録されていません。", "xpack.actions.actionTypeRegistry.register.duplicateActionTypeErrorMessage": "アクションタイプ \"{id}\" は既に登録されています。", "xpack.actions.appName": "アクション", - "xpack.actions.builtin.case.common.externalIncidentAdded": "({date}に{user}が追加)", - "xpack.actions.builtin.case.common.externalIncidentCreated": "({date}に{user}が作成)", - "xpack.actions.builtin.case.common.externalIncidentDefault": "({date}に{user}が作成)", - "xpack.actions.builtin.case.common.externalIncidentUpdated": "({date}に{user}が更新)", - "xpack.actions.builtin.case.configuration.apiWhitelistError": "コネクターアクションの構成エラー:{message}", - "xpack.actions.builtin.case.configuration.emptyMapping": "[casesConfiguration.mapping]:空以外の値が必要ですが空でした", - "xpack.actions.builtin.case.connectorApiNullError": "コネクター[apiUrl]が必要です", "xpack.actions.builtin.case.jiraTitle": "Jira", "xpack.actions.builtin.case.resilientTitle": "IBM Resilient", "xpack.actions.builtin.configuration.apiAllowedHostsError": "コネクターアクションの構成エラー:{message}", @@ -4599,7 +4592,6 @@ "xpack.actions.builtin.esIndex.errorIndexingErrorMessage": "エラーインデックス作成ドキュメント", "xpack.actions.builtin.esIndexTitle": "インデックス", "xpack.actions.builtin.jira.configuration.apiAllowedHostsError": "コネクターアクションの構成エラー:{message}", - "xpack.actions.builtin.jira.configuration.emptyMapping": "[incidentConfiguration.mapping]:空以外の値が必要ですが空でした", "xpack.actions.builtin.pagerduty.invalidTimestampErrorMessage": "タイムスタンプ\"{timestamp}\"の解析エラー", "xpack.actions.builtin.pagerduty.missingDedupkeyErrorMessage": "eventActionが「{eventAction}」のときにはDedupKeyが必要です", "xpack.actions.builtin.pagerduty.pagerdutyConfigurationError": "pagerduty アクションの設定エラー: {message}", @@ -4610,7 +4602,6 @@ "xpack.actions.builtin.pagerdutyTitle": "PagerDuty", "xpack.actions.builtin.serverLog.errorLoggingErrorMessage": "メッセージのロギングエラー", "xpack.actions.builtin.serverLogTitle": "サーバーログ", - "xpack.actions.builtin.servicenow.configuration.emptyMapping": "[incidentConfiguration.mapping]:空以外の値が必要ですが空でした", "xpack.actions.builtin.servicenowTitle": "ServiceNow", "xpack.actions.builtin.slack.errorPostingErrorMessage": "slack メッセージの投稿エラー", "xpack.actions.builtin.slack.errorPostingRetryDateErrorMessage": "slack メッセージの投稿エラー、 {retryString} に再試行", @@ -16371,14 +16362,11 @@ "xpack.securitySolution.case.configureCases.caseClosureOptionsManual": "セキュリティケースを手動で閉じる", "xpack.securitySolution.case.configureCases.caseClosureOptionsNewIncident": "新しいインシデントを外部システムにプッシュするときにセキュリティケースを自動的に閉じる", "xpack.securitySolution.case.configureCases.caseClosureOptionsTitle": "ケースのクローズ", - "xpack.securitySolution.case.configureCases.fieldMappingDesc": "データをサードパーティにプッシュするときにセキュリティケースフィールドをマップします。フィールドマッピングのためには、外部のインシデント管理システムへの接続を確立する必要があります。", "xpack.securitySolution.case.configureCases.fieldMappingEditAppend": "末尾に追加", "xpack.securitySolution.case.configureCases.fieldMappingEditNothing": "何もしない", "xpack.securitySolution.case.configureCases.fieldMappingEditOverwrite": "上書き", "xpack.securitySolution.case.configureCases.fieldMappingFirstCol": "セキュリティケースフィールド", - "xpack.securitySolution.case.configureCases.fieldMappingSecondCol": "外部インシデントフィールド", "xpack.securitySolution.case.configureCases.fieldMappingThirdCol": "編集時と更新時", - "xpack.securitySolution.case.configureCases.fieldMappingTitle": "フィールドマッピング", "xpack.securitySolution.case.configureCases.headerTitle": "ケースを構成", "xpack.securitySolution.case.configureCases.incidentManagementSystemDesc": "オプションとして、セキュリティケースを選択した外部のインシデント管理システムに接続できます。そうすると、選択したサードパーティシステム内でケースデータをインシデントとしてプッシュできます。", "xpack.securitySolution.case.configureCases.incidentManagementSystemLabel": "インシデント管理システム", @@ -19273,16 +19261,7 @@ "xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle": "タイミング", "xpack.triggersActionsUI.components.addMessageVariables.addVariablePopoverButton": "変数を追加", "xpack.triggersActionsUI.components.addMessageVariables.addVariableTitle": "アラート変数を追加", - "xpack.triggersActionsUI.components.builtinActionTypes.addNewConnector": "新しいコネクターを追加", - "xpack.triggersActionsUI.components.builtinActionTypes.cancelButton": "キャンセル", - "xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsClosedIncident": "新しいインシデントが外部システムで閉じたときにセキュリティケースを自動的に閉じる", - "xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsDesc": "セキュリティケースの終了のしかたを定義します。自動ケース終了のためには、外部のインシデント管理システムへの接続を確立する必要がいります。", - "xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsLabel": "ケース終了オプション", - "xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsManual": "セキュリティケースを手動で閉じる", - "xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsNewIncident": "新しいインシデントを外部システムにプッシュするときにセキュリティケースを自動的に閉じる", - "xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsTitle": "ケースのクローズ", "xpack.triggersActionsUI.components.builtinActionTypes.common.requiredDescriptionTextField": "説明が必要です。", - "xpack.triggersActionsUI.components.builtinActionTypes.common.requiredTitleTextField": "タイトルが必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.actionTypeTitle": "メールに送信", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.configureAccountsHelpLabel": "電子メールアカウントを構成しています。", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText": "サーバーからメールを送信します。", @@ -19301,17 +19280,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSubjectText": "件名が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredUserText": "パスワードの使用時にはユーザー名が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText": "本文が必要です。", - "xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingDesc": "データをサードパーティにプッシュするときにセキュリティケースフィールドをマップします。フィールドマッピングのためには、外部のインシデント管理システムへの接続を確立する必要があります。", - "xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditAppend": "末尾に追加", - "xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditNothing": "何もしない", - "xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditOverwrite": "上書き", - "xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingFirstCol": "セキュリティケースフィールド", - "xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingSecondCol": "外部インシデントフィールド", - "xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingThirdCol": "編集時と更新時", - "xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingTitle": "フィールドマッピング", - "xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemDesc": "オプションとして、セキュリティケースを選択した外部のインシデント管理システムに接続できます。そうすると、選択したサードパーティシステム内でケースデータをインシデントとしてプッシュできます。", - "xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemLabel": "インシデント管理システム", - "xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemTitle": "外部のインシデント管理システムに接続", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.actionTypeTitle": "データをインデックスする", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.chooseLabel": "選択...", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.configureIndexHelpLabel": "インデックスコネクターを構成しています。", @@ -19346,21 +19314,16 @@ "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredDescriptionTextField": "説明が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField": "電子メールアドレスまたはユーザー名が必要です", "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredProjectKeyTextField": "プロジェクトキーが必要です", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.savedObjectIdFieldHelp": "JIRAは、このアクションを、Kibanaの保存されたオブジェクトのIDに関連付けます。", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.savedObjectIdFieldLabel": "オブジェクトID(任意)", "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxAriaLabel": "親問題を選択", "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxPlaceholder": "親問題を選択", "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesLoading": "読み込み中…", "xpack.triggersActionsUI.components.builtinActionTypes.jira.selectMessageText": "Jiraでデータを更新するか、新しい問題にプッシュ", "xpack.triggersActionsUI.components.builtinActionTypes.jira.severitySelectFieldLabel": "優先度", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.titleFieldLabel": "まとめ", "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetFieldsMessage": "フィールドを取得できません", "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssueMessage": "ID {id}の問題を取得できません", "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssuesMessage": "問題を取得できません", "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssueTypesMessage": "問題タイプを取得できません", "xpack.triggersActionsUI.components.builtinActionTypes.jira.urgencySelectFieldLabel": "問題タイプ", - "xpack.triggersActionsUI.components.builtinActionTypes.mappingFieldNotMapped": "マップされません", - "xpack.triggersActionsUI.components.builtinActionTypes.noConnector": "コネクターを選択していません", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle": "PagerDuty に送信", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.apiUrlTextFieldLabel": "API URL(任意)", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.classFieldLabel": "クラス (任意)", @@ -19401,11 +19364,8 @@ "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeySecretTextField": "APIキーシークレットが必要です", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiUrlTextField": "URLが必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredOrgIdTextField": "組織IDが必要です", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.savedObjectIdFieldHelp": "IBM Resilientは、このアクションを、Kibanaの保存されたオブジェクトのIDに関連付けます。", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.savedObjectIdFieldLabel": "オブジェクトID(任意)", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText": "Resilientでデータを更新するか、または新しいインシデントにプッシュします。", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.severity": "深刻度", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.titleFieldLabel": "名前", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetIncidentTypesMessage": "インシデントタイプを取得できません", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetSeverityMessage": "深刻度を取得できません", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.urgencySelectFieldLabel": "インシデントタイプ", @@ -19430,8 +19390,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredEmailTextField": "電子メールが必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredPasswordTextField": "パスワードが必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUsernameTextField": "ユーザー名が必要です。", - "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.savedObjectIdFieldHelp": "ServiceNowは、このアクションを、Kibanaの保存されたオブジェクトのIDに関連付けます。", - "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.savedObjectIdFieldLabel": "オブジェクトID(任意)", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.selectMessageText": "ServiceNowでインシデントを作成します。", "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.severitySelectFieldLabel": "深刻度", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectHighOptionLabel": "高", @@ -19448,10 +19406,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.selectMessageText": "Slack チャネルにメッセージを送信します。", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlHelpLabel": "Slack Web フック URL を作成", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel": "Web フック URL", - "xpack.triggersActionsUI.components.builtinActionTypes.updateConnector": "コネクターを更新", - "xpack.triggersActionsUI.components.builtinActionTypes.updateSelectedConnector": "{ connectorName }を更新", - "xpack.triggersActionsUI.components.builtinActionTypes.warningMessage": "選択したコネクターが削除されました。別のコネクターを選択するか、新しいコネクターを作成してください。", - "xpack.triggersActionsUI.components.builtinActionTypes.warningTitle": "警告", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.actionTypeTitle": "Web フックデータ", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeader": "ヘッダーを追加", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeaderButton": "追加", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c0fc0540c2cd7..32f61b37b7b8d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4586,13 +4586,6 @@ "xpack.actions.actionTypeRegistry.get.missingActionTypeErrorMessage": "未注册操作类型“{id}”。", "xpack.actions.actionTypeRegistry.register.duplicateActionTypeErrorMessage": "操作类型“{id}”已注册。", "xpack.actions.appName": "操作", - "xpack.actions.builtin.case.common.externalIncidentAdded": "(由 {user} 于 {date}添加)", - "xpack.actions.builtin.case.common.externalIncidentCreated": "(由 {user} 于 {date}创建)", - "xpack.actions.builtin.case.common.externalIncidentDefault": "(由 {user} 于 {date}创建)", - "xpack.actions.builtin.case.common.externalIncidentUpdated": "(由 {user} 于 {date}更新)", - "xpack.actions.builtin.case.configuration.apiWhitelistError": "配置连接器操作时出错:{message}", - "xpack.actions.builtin.case.configuration.emptyMapping": "[casesConfiguration.mapping]:应为非空,但却为空", - "xpack.actions.builtin.case.connectorApiNullError": "需要指定连接器 [apiUrl]", "xpack.actions.builtin.case.jiraTitle": "Jira", "xpack.actions.builtin.case.resilientTitle": "IBM Resilient", "xpack.actions.builtin.configuration.apiAllowedHostsError": "配置连接器操作时出错:{message}", @@ -4601,7 +4594,6 @@ "xpack.actions.builtin.esIndex.errorIndexingErrorMessage": "索引文档时出错", "xpack.actions.builtin.esIndexTitle": "索引", "xpack.actions.builtin.jira.configuration.apiAllowedHostsError": "配置连接器操作时出错:{message}", - "xpack.actions.builtin.jira.configuration.emptyMapping": "[incidentConfiguration.mapping]:应为非空,但却为空", "xpack.actions.builtin.pagerduty.invalidTimestampErrorMessage": "解析时间戳“{timestamp}”时出错", "xpack.actions.builtin.pagerduty.missingDedupkeyErrorMessage": "当 eventAction 是“{eventAction}”时需要 DedupKey", "xpack.actions.builtin.pagerduty.pagerdutyConfigurationError": "配置 pagerduty 操作时出错:{message}", @@ -4612,7 +4604,6 @@ "xpack.actions.builtin.pagerdutyTitle": "PagerDuty", "xpack.actions.builtin.serverLog.errorLoggingErrorMessage": "记录消息时出错", "xpack.actions.builtin.serverLogTitle": "服务器日志", - "xpack.actions.builtin.servicenow.configuration.emptyMapping": "[incidentConfiguration.mapping]:应为非空,但却为空", "xpack.actions.builtin.servicenowTitle": "ServiceNow", "xpack.actions.builtin.slack.errorPostingErrorMessage": "发布 slack 消息时出错", "xpack.actions.builtin.slack.errorPostingRetryDateErrorMessage": "发布 slack 消息时出错,在 {retryString} 重试", @@ -16388,14 +16379,11 @@ "xpack.securitySolution.case.configureCases.caseClosureOptionsManual": "手动关闭 Security 案例", "xpack.securitySolution.case.configureCases.caseClosureOptionsNewIncident": "将新事件推送到外部系统时自动关闭 Security 案例", "xpack.securitySolution.case.configureCases.caseClosureOptionsTitle": "案例关闭", - "xpack.securitySolution.case.configureCases.fieldMappingDesc": "将数据推送到第三方时映射 Security 案例字段。要映射字段,需要与外部事件管理系统建立连接。", "xpack.securitySolution.case.configureCases.fieldMappingEditAppend": "追加", "xpack.securitySolution.case.configureCases.fieldMappingEditNothing": "无内容", "xpack.securitySolution.case.configureCases.fieldMappingEditOverwrite": "覆盖", "xpack.securitySolution.case.configureCases.fieldMappingFirstCol": "Security 案例字段", - "xpack.securitySolution.case.configureCases.fieldMappingSecondCol": "外部事件字段", "xpack.securitySolution.case.configureCases.fieldMappingThirdCol": "编辑和更新时", - "xpack.securitySolution.case.configureCases.fieldMappingTitle": "字段映射", "xpack.securitySolution.case.configureCases.headerTitle": "配置案例", "xpack.securitySolution.case.configureCases.incidentManagementSystemDesc": "您可能会根据需要将 Security 案例连接到选择的外部事件管理系统。这将允许您将案例数据作为事件推送到所选第三方系统。", "xpack.securitySolution.case.configureCases.incidentManagementSystemLabel": "事件管理系统", @@ -19291,16 +19279,7 @@ "xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle": "当", "xpack.triggersActionsUI.components.addMessageVariables.addVariablePopoverButton": "添加变量", "xpack.triggersActionsUI.components.addMessageVariables.addVariableTitle": "添加告警变量", - "xpack.triggersActionsUI.components.builtinActionTypes.addNewConnector": "添加新连接器", - "xpack.triggersActionsUI.components.builtinActionTypes.cancelButton": "取消", - "xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsClosedIncident": "在外部系统中关闭事件时自动关闭 Security 案例", - "xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsDesc": "定义关闭 Security 案例的方式。要自动关闭案例,需要与外部事件管理系统建立连接。", - "xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsLabel": "案例关闭选项", - "xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsManual": "手动关闭 Security 案例", - "xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsNewIncident": "将新事件推送到外部系统时自动关闭 Security 案例", - "xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsTitle": "案例关闭", "xpack.triggersActionsUI.components.builtinActionTypes.common.requiredDescriptionTextField": "描述必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.common.requiredTitleTextField": "标题必填。", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.actionTypeTitle": "发送到电子邮件", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.configureAccountsHelpLabel": "配置电子邮件帐户。", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText": "从您的服务器发送电子邮件。", @@ -19319,17 +19298,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSubjectText": "“主题”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredUserText": "使用密码时,“用户名”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText": "“正文”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingDesc": "将数据推送到第三方时映射 Security 案例字段。要映射字段,需要与外部事件管理系统建立连接。", - "xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditAppend": "追加", - "xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditNothing": "无内容", - "xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditOverwrite": "覆盖", - "xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingFirstCol": "Security 案例字段", - "xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingSecondCol": "外部事件字段", - "xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingThirdCol": "编辑和更新时", - "xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingTitle": "字段映射", - "xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemDesc": "您可能会根据需要将 Security 案例连接到选择的外部事件管理系统。这将允许您将案例数据作为事件推送到所选第三方系统。", - "xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemLabel": "事件管理系统", - "xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemTitle": "连接到外部事件管理系统", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.actionTypeTitle": "索引数据", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.chooseLabel": "选择……", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.configureIndexHelpLabel": "配置索引连接器。", @@ -19364,21 +19332,16 @@ "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredDescriptionTextField": "“描述”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField": "“电子邮件”或“用户名”必填", "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredProjectKeyTextField": "“项目键”必填", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.savedObjectIdFieldHelp": "JIRA 将此操作与 Kibana 已保存对象的 ID 关联。", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.savedObjectIdFieldLabel": "对象 ID(可选)", "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxAriaLabel": "选择父问题", "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxPlaceholder": "选择父问题", "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesLoading": "正在加载……", "xpack.triggersActionsUI.components.builtinActionTypes.jira.selectMessageText": "将数据推送或更新到 Jira 中的新问题", "xpack.triggersActionsUI.components.builtinActionTypes.jira.severitySelectFieldLabel": "优先级", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.titleFieldLabel": "摘要", "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetFieldsMessage": "无法获取字段", "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssueMessage": "无法获取 ID 为 {id} 的问题", "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssuesMessage": "无法获取问题", "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssueTypesMessage": "无法获取问题类型", "xpack.triggersActionsUI.components.builtinActionTypes.jira.urgencySelectFieldLabel": "问题类型", - "xpack.triggersActionsUI.components.builtinActionTypes.mappingFieldNotMapped": "未映射", - "xpack.triggersActionsUI.components.builtinActionTypes.noConnector": "未选择连接器", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle": "发送到 PagerDuty", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.apiUrlTextFieldLabel": "API URL(可选)", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.classFieldLabel": "类(可选)", @@ -19419,11 +19382,8 @@ "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeySecretTextField": "“API 密钥密码”必填", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiUrlTextField": "“URL”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredOrgIdTextField": "“组织 ID”必填", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.savedObjectIdFieldHelp": "IBM Resilient 将此操作与 Kibana 已保存对象的 ID 关联。", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.savedObjectIdFieldLabel": "对象 ID(可选)", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText": "将数据推送或更新到 Resilient 中的新事件。", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.severity": "严重性", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.titleFieldLabel": "名称", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetIncidentTypesMessage": "无法获取事件类型", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetSeverityMessage": "无法获取严重性", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.urgencySelectFieldLabel": "事件类型", @@ -19448,8 +19408,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredEmailTextField": "“电子邮件”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredPasswordTextField": "“密码”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUsernameTextField": "“用户名”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.savedObjectIdFieldHelp": "ServiceNow 将此操作与 Kibana 已保存对象的 ID 关联。", - "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.savedObjectIdFieldLabel": "对象 ID(可选)", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.selectMessageText": "在 ServiceNow 中创建事件。", "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.severitySelectFieldLabel": "严重性", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectHighOptionLabel": "高", @@ -19466,10 +19424,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.selectMessageText": "向 Slack 频道或用户发送消息。", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlHelpLabel": "创建 Slack webhook URL", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel": "Webhook URL", - "xpack.triggersActionsUI.components.builtinActionTypes.updateConnector": "更新连接器", - "xpack.triggersActionsUI.components.builtinActionTypes.updateSelectedConnector": "更新 { connectorName }", - "xpack.triggersActionsUI.components.builtinActionTypes.warningMessage": "选定的连接器已删除。选择不同的连接器或创建新的连接器。", - "xpack.triggersActionsUI.components.builtinActionTypes.warningTitle": "警告", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.actionTypeTitle": "Webhook 数据", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeader": "添加标头", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeaderButton": "添加", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping.tsx deleted file mode 100644 index a3382513d2bcb..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping.tsx +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback, useMemo } from 'react'; -import { EuiFormRow, EuiFlexItem, EuiFlexGroup, EuiSuperSelectOption } from '@elastic/eui'; -import styled from 'styled-components'; - -import { FieldMappingRow } from './field_mapping_row'; -import * as i18n from './translations'; - -import { setActionTypeToMapping, setThirdPartyToMapping } from './utils'; -import { ThirdPartyField as ConnectorConfigurationThirdPartyField } from './types'; -import { CasesConfigurationMapping } from './types'; -import { createDefaultMapping } from './utils'; - -const FieldRowWrapper = styled.div` - margin-top: 8px; - font-size: 14px; -`; - -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 getThirdPartyOptions = ( - caseField: string, - thirdPartyFields: Record -): Array> => - (Object.keys(thirdPartyFields) as string[]).reduce>>( - (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; - onChangeMapping: (newMapping: CasesConfigurationMapping[]) => void; - connectorConfiguration: Record; -} - -const FieldMappingComponent: React.FC = ({ - disabled, - mapping, - onChangeMapping, - connectorConfiguration, -}) => { - const onChangeActionType = useCallback( - (caseField: string, newActionType: string) => { - const myMapping = mapping ?? defaultMapping; - onChangeMapping(setActionTypeToMapping(caseField, newActionType, myMapping)); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [mapping] - ); - - const onChangeThirdParty = useCallback( - (caseField: string, newThirdPartyField: string) => { - const myMapping = mapping ?? defaultMapping; - onChangeMapping(setThirdPartyToMapping(caseField, newThirdPartyField, myMapping)); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [mapping] - ); - - const selectedConnector = connectorConfiguration ?? { fields: {} }; - const defaultMapping = useMemo(() => createDefaultMapping(selectedConnector.fields), [ - selectedConnector.fields, - ]); - - return ( - <> - - - - {i18n.FIELD_MAPPING_FIRST_COL} - - - {i18n.FIELD_MAPPING_SECOND_COL} - - - {i18n.FIELD_MAPPING_THIRD_COL} - - - - - {(mapping ?? defaultMapping).map((item) => ( - - ))} - - - ); -}; - -export const FieldMapping = React.memo(FieldMappingComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping_row.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping_row.tsx deleted file mode 100644 index beca8f1fbbc77..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping_row.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useMemo } from 'react'; -import { - EuiFlexItem, - EuiFlexGroup, - EuiSuperSelect, - EuiIcon, - EuiSuperSelectOption, -} from '@elastic/eui'; - -import { capitalize } from 'lodash'; - -export interface RowProps { - id: string; - disabled: boolean; - securitySolutionField: string; - thirdPartyOptions: Array>; - actionTypeOptions: Array>; - onChangeActionType: (caseField: string, newActionType: string) => void; - onChangeThirdParty: (caseField: string, newThirdPartyField: string) => void; - selectedActionType: string; - selectedThirdParty: string; -} - -const FieldMappingRowComponent: React.FC = ({ - id, - disabled, - securitySolutionField, - thirdPartyOptions, - actionTypeOptions, - onChangeActionType, - onChangeThirdParty, - selectedActionType, - selectedThirdParty, -}) => { - const securitySolutionFieldCapitalized = useMemo(() => capitalize(securitySolutionField), [ - securitySolutionField, - ]); - return ( - - - - - {securitySolutionFieldCapitalized} - - - - - - - - - - - - - - ); -}; - -export const FieldMappingRow = React.memo(FieldMappingRowComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/index.ts deleted file mode 100644 index 2de9b87ead3fe..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export * from './types'; -export * from './field_mapping'; -export * from './field_mapping_row'; -export * from './utils'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/translations.ts deleted file mode 100644 index 665ccbcfa114d..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/translations.ts +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const INCIDENT_MANAGEMENT_SYSTEM_TITLE = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemTitle', - { - defaultMessage: 'Connect to external incident management system', - } -); - -export const INCIDENT_MANAGEMENT_SYSTEM_DESC = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemDesc', - { - defaultMessage: - 'You may optionally connect Security cases to an external incident management system of your choosing. This will allow you to push case data as an incident in your chosen third-party system.', - } -); - -export const INCIDENT_MANAGEMENT_SYSTEM_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemLabel', - { - defaultMessage: 'Incident management system', - } -); - -export const NO_CONNECTOR = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.noConnector', - { - defaultMessage: 'No connector selected', - } -); - -export const ADD_NEW_CONNECTOR = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.addNewConnector', - { - defaultMessage: 'Add new connector', - } -); - -export const CASE_CLOSURE_OPTIONS_TITLE = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsTitle', - { - defaultMessage: 'Case Closures', - } -); - -export const CASE_CLOSURE_OPTIONS_DESC = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsDesc', - { - defaultMessage: - 'Define how you wish Security cases to be closed. Automated case closures require an established connection to an external incident management system.', - } -); - -export const CASE_CLOSURE_OPTIONS_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsLabel', - { - defaultMessage: 'Case closure options', - } -); - -export const CASE_CLOSURE_OPTIONS_MANUAL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsManual', - { - defaultMessage: 'Manually close Security cases', - } -); - -export const CASE_CLOSURE_OPTIONS_NEW_INCIDENT = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsNewIncident', - { - defaultMessage: - 'Automatically close Security cases when pushing new incident to external system', - } -); - -export const CASE_CLOSURE_OPTIONS_CLOSED_INCIDENT = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsClosedIncident', - { - defaultMessage: 'Automatically close Security cases when incident is closed in external system', - } -); - -export const FIELD_MAPPING_TITLE = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingTitle', - { - defaultMessage: 'Field mappings', - } -); - -export const FIELD_MAPPING_DESC = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingDesc', - { - defaultMessage: - 'Map Security case fields when pushing data to a third-party. Field mappings require an established connection to an external incident management system.', - } -); - -export const FIELD_MAPPING_FIRST_COL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingFirstCol', - { - defaultMessage: 'Security case field', - } -); - -export const FIELD_MAPPING_SECOND_COL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingSecondCol', - { - defaultMessage: 'External incident field', - } -); - -export const FIELD_MAPPING_THIRD_COL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingThirdCol', - { - defaultMessage: 'On edit and update', - } -); - -export const FIELD_MAPPING_EDIT_NOTHING = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditNothing', - { - defaultMessage: 'Nothing', - } -); - -export const FIELD_MAPPING_EDIT_OVERWRITE = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditOverwrite', - { - defaultMessage: 'Overwrite', - } -); - -export const FIELD_MAPPING_EDIT_APPEND = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditAppend', - { - defaultMessage: 'Append', - } -); - -export const CANCEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.cancelButton', - { - defaultMessage: 'Cancel', - } -); - -export const WARNING_NO_CONNECTOR_TITLE = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.warningTitle', - { - defaultMessage: 'Warning', - } -); - -export const WARNING_NO_CONNECTOR_MESSAGE = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.warningMessage', - { - defaultMessage: - 'The selected connector has been deleted. Either select a different connector or create a new one.', - } -); - -export const MAPPING_FIELD_NOT_MAPPED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.mappingFieldNotMapped', - { - defaultMessage: 'Not mapped', - } -); - -export const UPDATE_CONNECTOR = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.updateConnector', - { - defaultMessage: 'Update connector', - } -); - -export const UPDATE_SELECTED_CONNECTOR = (connectorName: string): string => { - return i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.updateSelectedConnector', - { - values: { connectorName }, - defaultMessage: 'Update { connectorName }', - } - ); -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/types.ts deleted file mode 100644 index 3571db39b596a..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ActionType } from '../../../../types'; - -export { ActionType }; - -export interface ThirdPartyField { - label: string; - validSourceFields: string[]; - defaultSourceField: string; - defaultActionType: string; -} -export interface CasesConfigurationMapping { - source: string; - target: string; - actionType: string; -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/utils.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/utils.ts deleted file mode 100644 index b14b1b76427c6..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/utils.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { CasesConfigurationMapping } from './types'; - -export const setActionTypeToMapping = ( - caseField: string, - newActionType: string, - mapping: CasesConfigurationMapping[] -): CasesConfigurationMapping[] => { - const findItemIndex = mapping.findIndex((item) => item.source === caseField); - - if (findItemIndex >= 0) { - return [ - ...mapping.slice(0, findItemIndex), - { ...mapping[findItemIndex], actionType: newActionType }, - ...mapping.slice(findItemIndex + 1), - ]; - } - - return [...mapping]; -}; - -export const setThirdPartyToMapping = ( - caseField: string, - newThirdPartyField: string, - mapping: CasesConfigurationMapping[] -): CasesConfigurationMapping[] => - mapping.map((item) => { - if (item.source !== caseField && item.target === newThirdPartyField) { - return { ...item, target: 'not_mapped' }; - } else if (item.source === caseField) { - return { ...item, target: newThirdPartyField }; - } - return item; - }); - -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/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx index b10341fa00f1b..95c2db0948ea3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx @@ -78,22 +78,22 @@ describe('jira connector validation', () => { describe('jira action params validation', () => { test('action params validation succeeds when action params is valid', () => { const actionParams = { - subActionParams: { title: 'some title {{test}}' }, + subActionParams: { incident: { summary: 'some title {{test}}' }, comments: [] }, }; expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { title: [] }, + errors: { summary: [] }, }); }); test('params validation fails when body is not valid', () => { const actionParams = { - subActionParams: { title: '' }, + subActionParams: { incident: { summary: '' }, comments: [] }, }; expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { - title: ['Summary is required.'], + summary: ['Summary is required.'], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx index 20374cfbe3a3b..954623939cd3b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx @@ -61,11 +61,15 @@ export function getActionType(): ActionTypeModel { const validationResult = { errors: {} }; const errors = { - title: new Array(), + summary: new Array(), }; validationResult.errors = errors; - if (!actionParams.subActionParams?.title?.length) { - errors.title.push(i18n.SUMMARY_REQUIRED); + if ( + actionParams.subActionParams && + actionParams.subActionParams.incident && + !actionParams.subActionParams.incident.summary?.length + ) { + errors.summary.push(i18n.SUMMARY_REQUIRED); } return validationResult; }, 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 e3f9bb99b48b6..35e0b099fe004 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 @@ -74,7 +74,6 @@ describe('JiraActionConnectorFields renders', () => { consumer={'case'} /> ); - expect(wrapper.find('[data-test-subj="case-jira-mappings"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); expect( wrapper.find('[data-test-subj="connector-jira-project-key-form-input"]').length > 0 diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx index f32b521797f58..fea398002fddd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx @@ -17,25 +17,19 @@ import { EuiTitle, } from '@elastic/eui'; -import { isEmpty } from 'lodash'; import { ActionConnectorFieldsProps } from '../../../../types'; -import { CasesConfigurationMapping, FieldMapping, createDefaultMapping } from '../case_mappings'; import * as i18n from './translations'; import { JiraActionConnector } from './types'; -import { connectorConfiguration } from './config'; const JiraConnectorFields: React.FC> = ({ action, editActionSecrets, editActionConfig, errors, - consumer, readOnly, }) => { - // TODO: remove incidentConfiguration later, when Case Jira will move their fields to the level of action execution - const { apiUrl, projectKey, incidentConfiguration, isCaseOwned } = action.config; - const mapping = incidentConfiguration ? incidentConfiguration.mapping : []; + const { apiUrl, projectKey } = action.config; const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; @@ -45,38 +39,27 @@ const JiraConnectorFields: React.FC 0 && email != null; const isApiTokenInvalid: boolean = errors.apiToken.length > 0 && apiToken != null; - // TODO: remove this block later, when Case ServiceNow will move their fields to the level of action execution - if (consumer === 'case') { - if (isEmpty(mapping)) { - editActionConfig('incidentConfiguration', { - mapping: createDefaultMapping(connectorConfiguration.fields as any), - }); - } - if (!isCaseOwned) { - editActionConfig('isCaseOwned', true); - } - } - const handleOnChangeActionConfig = useCallback( (key: string, value: string) => editActionConfig(key, value), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [editActionConfig] ); const handleOnChangeSecretConfig = useCallback( (key: string, value: string) => editActionSecrets(key, value), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [editActionSecrets] ); - const handleOnChangeMappingConfig = useCallback( - (newMapping: CasesConfigurationMapping[]) => - editActionConfig('incidentConfiguration', { - ...action.config.incidentConfiguration, - mapping: newMapping, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [action.config] + const handleResetField = useCallback( + (checkValue, fieldName: string, actionField: 'config' | 'secrets') => { + if (!checkValue) { + if (actionField === 'config') { + handleOnChangeActionConfig(fieldName, ''); + } else { + handleOnChangeSecretConfig(fieldName, ''); + } + } + }, + [handleOnChangeActionConfig, handleOnChangeSecretConfig] ); return ( @@ -98,11 +81,7 @@ const JiraConnectorFields: React.FC handleOnChangeActionConfig('apiUrl', evt.target.value)} - onBlur={() => { - if (!apiUrl) { - editActionConfig('apiUrl', ''); - } - }} + onBlur={() => handleResetField(apiUrl, 'apiUrl', 'config')} /> @@ -124,11 +103,7 @@ const JiraConnectorFields: React.FC handleOnChangeActionConfig('projectKey', evt.target.value)} - onBlur={() => { - if (!projectKey) { - editActionConfig('projectKey', ''); - } - }} + onBlur={() => handleResetField(projectKey, 'projectKey', 'config')} /> @@ -165,11 +140,7 @@ const JiraConnectorFields: React.FC handleOnChangeSecretConfig('email', evt.target.value)} - onBlur={() => { - if (!email) { - editActionSecrets('email', ''); - } - }} + onBlur={() => handleResetField(email, 'email', 'secrets')} /> @@ -192,30 +163,11 @@ const JiraConnectorFields: React.FC handleOnChangeSecretConfig('apiToken', evt.target.value)} - onBlur={() => { - if (!apiToken) { - editActionSecrets('apiToken', ''); - } - }} + onBlur={() => handleResetField(apiToken, 'apiToken', 'secrets')} /> - {consumer === 'case' && ( // TODO: remove this block later, when Case Jira will move their fields to the level of action execution - <> - - - - - - - - )} ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx index e7bec0b4b4452..5da629efefdc6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx @@ -21,15 +21,16 @@ const useGetFieldsByIssueTypeMock = useGetFieldsByIssueType as jest.Mock; const actionParams = { subAction: 'pushToService', subActionParams: { - title: 'sn title', - description: 'some description', + incident: { + summary: 'sn title', + description: 'some description', + issueType: '10006', + labels: ['kibana'], + priority: 'High', + externalId: null, + parent: null, + }, comments: [{ commentId: '1', comment: 'comment for jira' }], - issueType: '10006', - labels: ['kibana'], - priority: 'High', - savedObjectId: '123', - externalId: null, - parent: null, }, }; @@ -84,7 +85,7 @@ describe('JiraParamsFields renders', () => { const wrapper = mountWithIntl( {}} index={0} messageVariables={[{ name: AlertProvidedActionVariables.alertId, description: '' }]} @@ -97,28 +98,10 @@ describe('JiraParamsFields renders', () => { expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('value')).toStrictEqual( 'High' ); - expect(wrapper.find('[data-test-subj="titleInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="summaryInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="descriptionTextArea"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="labelsComboBox"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="commentsTextArea"]').length > 0).toBeTruthy(); - - // ensure savedObjectIdInput isnt rendered - expect(wrapper.find('[data-test-subj="savedObjectIdInput"]').length === 0).toBeTruthy(); - }); - - test('the savedObjectId fields is rendered if we cant find an alertId in the messageVariables', () => { - const wrapper = mountWithIntl( - {}} - index={0} - messageVariables={[]} - actionConnector={connector} - /> - ); - - expect(wrapper.find('[data-test-subj="savedObjectIdInput"]').length > 0).toBeTruthy(); }); test('it shows loading when loading issue types', () => { @@ -126,7 +109,7 @@ describe('JiraParamsFields renders', () => { const wrapper = mountWithIntl( {}} index={0} messageVariables={[]} @@ -148,7 +131,7 @@ describe('JiraParamsFields renders', () => { const wrapper = mountWithIntl( {}} index={0} messageVariables={[]} @@ -170,7 +153,7 @@ describe('JiraParamsFields renders', () => { const wrapper = mountWithIntl( {}} index={0} messageVariables={[]} @@ -196,7 +179,7 @@ describe('JiraParamsFields renders', () => { const wrapper = mountWithIntl( {}} index={0} messageVariables={[]} @@ -222,7 +205,7 @@ describe('JiraParamsFields renders', () => { const wrapper = mountWithIntl( {}} index={0} messageVariables={[]} @@ -231,7 +214,7 @@ describe('JiraParamsFields renders', () => { ); expect(wrapper.find('[data-test-subj="issueTypeSelect"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="titleInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="summaryInput"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="commentsTextArea"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="prioritySelect"]').exists()).toBeFalsy(); 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 aaa9b697f32ec..91bab3bc3eb97 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 @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useEffect, useState, useMemo } from 'react'; -import { map } from 'lodash/fp'; -import { isSome } from 'fp-ts/lib/Option'; +import React, { Fragment, useCallback, useEffect, useMemo, useRef } from 'react'; + import { i18n } from '@kbn/i18n'; import { EuiFormRow, @@ -14,8 +13,6 @@ import { EuiSelectOption, EuiHorizontalRule, EuiSelect, - EuiFormControlLayout, - EuiIconTip, EuiFlexGroup, EuiFlexItem, EuiSpacer, @@ -28,36 +25,30 @@ import { JiraActionParams } from './types'; import { useGetIssueTypes } from './use_get_issue_types'; import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type'; import { SearchIssues } from './search_issues'; -import { extractActionVariable } from '../extract_action_variable'; -import { AlertProvidedActionVariables } from '../../../lib/action_variables'; import { useKibana } from '../../../../common/lib/kibana'; const JiraParamsFields: React.FunctionComponent> = ({ + actionConnector, actionParams, editAction, - index, errors, + index, messageVariables, - actionConnector, }) => { const { http, notifications: { toasts }, } = useKibana().services; - const { title, description, comments, issueType, priority, labels, parent, savedObjectId } = - actionParams.subActionParams || {}; - - const [issueTypesSelectOptions, setIssueTypesSelectOptions] = useState([]); - const [firstLoad, setFirstLoad] = useState(false); - const [prioritiesSelectOptions, setPrioritiesSelectOptions] = useState([]); - - const isActionBeingConfiguredByAnAlert = messageVariables - ? isSome(extractActionVariable(messageVariables, AlertProvidedActionVariables.alertId)) - : false; - - useEffect(() => { - setFirstLoad(true); - }, []); + const { incident, comments } = useMemo( + () => + actionParams.subActionParams ?? + (({ + incident: {}, + comments: [], + } as unknown) as JiraActionParams['subActionParams']), + [actionParams.subActionParams] + ); + const actionConnectorRef = useRef(actionConnector?.id ?? ''); const { isLoading: isLoadingIssueTypes, issueTypes } = useGetIssueTypes({ http, @@ -69,8 +60,29 @@ const JiraParamsFields: React.FunctionComponent { + const newProps = + key !== 'comments' + ? { + incident: { ...incident, [key]: value }, + comments, + } + : { incident, [key]: value }; + editAction('subActionParams', newProps, index); + }, + [comments, editAction, incident, index] + ); + const editComment = useCallback( + (key, value) => { + if (value.length > 0) { + editSubActionProperty(key, [{ commentId: '1', comment: value }]); + } + }, + [editSubActionProperty] + ); const hasLabels = useMemo(() => Object.prototype.hasOwnProperty.call(fields, 'labels'), [fields]); const hasDescription = useMemo( @@ -81,98 +93,71 @@ const JiraParamsFields: React.FunctionComponent Object.prototype.hasOwnProperty.call(fields, 'parent'), [fields]); - - useEffect(() => { - const options = issueTypes.map((type) => ({ + const issueTypesSelectOptions: EuiSelectOption[] = useMemo(() => { + const doesIssueTypeExist = + incident.issueType != null && issueTypes.length + ? issueTypes.some((t) => t.id === incident.issueType) + : true; + if ((!incident.issueType || !doesIssueTypeExist) && issueTypes.length > 0) { + editSubActionProperty('issueType', issueTypes[0].id ?? ''); + } + return issueTypes.map((type) => ({ value: type.id ?? '', text: type.name ?? '', })); - - setIssueTypesSelectOptions(options); - }, [issueTypes]); - - useEffect(() => { - if (issueType != null && fields != null) { - const priorities = fields.priority?.allowedValues ?? []; - const options = map( - (p) => ({ + }, [editSubActionProperty, incident, issueTypes]); + const prioritiesSelectOptions: EuiSelectOption[] = useMemo(() => { + if (incident.issueType != null && fields != null) { + const priorities = fields.priority != null ? fields.priority.allowedValues : []; + const doesPriorityExist = priorities.some((p) => p.name === incident.priority); + if ((!incident.priority || !doesPriorityExist) && priorities.length > 0) { + editSubActionProperty('priority', priorities[0].name ?? ''); + } + return priorities.map((p: { id: string; name: string }) => { + return { value: p.name, text: p.name, - }), - priorities - ); - setPrioritiesSelectOptions(options); + }; + }); } - }, [fields, issueType]); + return []; + }, [editSubActionProperty, fields, incident.issueType, incident.priority]); - const labelOptions = useMemo(() => (labels ? labels.map((label: string) => ({ label })) : []), [ - labels, - ]); - - const editSubActionProperty = (key: string, value: any) => { - const newProps = { ...actionParams.subActionParams, [key]: value }; - editAction('subActionParams', newProps, index); - }; + const labelOptions = useMemo( + () => (incident.labels ? incident.labels.map((label: string) => ({ label })) : []), + [incident.labels] + ); - // Reset parameters when changing connector useEffect(() => { - if (!firstLoad) { - return; + if (actionConnector != null && actionConnectorRef.current !== actionConnector.id) { + actionConnectorRef.current = actionConnector.id; + editAction( + 'subActionParams', + { + incident: {}, + comments: [], + }, + index + ); } - - setIssueTypesSelectOptions([]); - editAction('subActionParams', { title, comments, description: '', savedObjectId }, index); // eslint-disable-next-line react-hooks/exhaustive-deps }, [actionConnector]); - - // Reset fields when changing connector or issue type - useEffect(() => { - if (!firstLoad) { - return; - } - - setPrioritiesSelectOptions([]); - editAction( - 'subActionParams', - { title, issueType, comments, description: '', savedObjectId }, - index - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [issueType, savedObjectId]); - useEffect(() => { if (!actionParams.subAction) { editAction('subAction', 'pushToService', index); } - if (!savedObjectId && isActionBeingConfiguredByAnAlert) { - editSubActionProperty('savedObjectId', `${AlertProvidedActionVariables.alertId}`); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - actionConnector, - actionParams.subAction, - index, - savedObjectId, - issueTypesSelectOptions, - issueType, - ]); - - // Set default issue type - useEffect(() => { - if (!issueType && issueTypesSelectOptions.length > 0) { - editSubActionProperty('issueType', issueTypesSelectOptions[0].value as string); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [issueTypes, issueTypesSelectOptions]); - - // Set default priority - useEffect(() => { - if (!priority && prioritiesSelectOptions.length > 0) { - editSubActionProperty('priority', prioritiesSelectOptions[0].value as string); + if (!actionParams.subActionParams) { + editAction( + 'subActionParams', + { + incident: {}, + comments: [], + }, + index + ); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [actionConnector, issueType, prioritiesSelectOptions]); - + }, [actionParams]); return ( <> @@ -191,10 +176,8 @@ const JiraParamsFields: React.FunctionComponent { - editSubActionProperty('issueType', e.target.value); - }} + value={incident.issueType ?? undefined} + onChange={(e) => editSubActionProperty('issueType', e.target.value)} /> @@ -212,7 +195,7 @@ const JiraParamsFields: React.FunctionComponent { editSubActionProperty('priority', e.target.value); }} @@ -259,12 +242,12 @@ const JiraParamsFields: React.FunctionComponent 0 && title !== undefined} + error={errors.summary} + isInvalid={errors.summary.length > 0 && incident.summary !== undefined} label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.titleFieldLabel', + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.summaryFieldLabel', { - defaultMessage: 'Summary', + defaultMessage: 'Summary (required)', } )} > @@ -272,51 +255,12 @@ const JiraParamsFields: React.FunctionComponent - {!isActionBeingConfiguredByAnAlert && ( - - - - - } - > - - - - - - - )} {hasLabels && ( <> @@ -326,7 +270,7 @@ const JiraParamsFields: React.FunctionComponent @@ -350,7 +294,7 @@ const JiraParamsFields: React.FunctionComponent { - if (!labels) { + if (!incident.labels) { editSubActionProperty('labels', []); } }} @@ -369,11 +313,11 @@ const JiraParamsFields: React.FunctionComponent { - editSubActionProperty(key, [{ commentId: '1', comment: value }]); - }} + editAction={editComment} messageVariables={messageVariables} paramsProperty={'comments'} - inputTargetValue={comments && comments.length > 0 ? comments[0].comment : ''} + inputTargetValue={comments && comments.length > 0 ? comments[0].comment : undefined} label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.commentsTextAreaFieldLabel', { - defaultMessage: 'Additional comments (optional)', + defaultMessage: 'Additional comments', } )} errors={errors.comments as string[]} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts index e72aa1f7fc037..50483f46e1084 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts @@ -4,40 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CasesConfigurationMapping } from '../case_mappings'; import { UserConfiguredActionConnector } from '../../../../types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ExecutorSubActionPushParams } from '../../../../../../actions/server/builtin_action_types/jira/types'; export type JiraActionConnector = UserConfiguredActionConnector; export interface JiraActionParams { subAction: string; - subActionParams: { - savedObjectId: string; - title: string; - description: string; - comments: Array<{ commentId: string; comment: string }>; - externalId: string | null; - issueType: string; - priority: string; - labels: string[]; - parent: string | null; - }; -} - -interface IncidentConfiguration { - mapping: CasesConfigurationMapping[]; + subActionParams: ExecutorSubActionPushParams; } export interface JiraConfig { apiUrl: string; projectKey: string; - incidentConfiguration?: IncidentConfiguration; - isCaseOwned?: boolean; } export interface JiraSecrets { email: string; apiToken: string; } - -// to remove diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx index 17e9b42e7878e..2857e5dabd506 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx @@ -78,22 +78,22 @@ describe('resilient connector validation', () => { describe('resilient action params validation', () => { test('action params validation succeeds when action params is valid', () => { const actionParams = { - subActionParams: { title: 'some title {{test}}' }, + subActionParams: { incident: { name: 'some title {{test}}' }, comments: [] }, }; expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { title: [] }, + errors: { name: [] }, }); }); test('params validation fails when body is not valid', () => { const actionParams = { - subActionParams: { title: '' }, + subActionParams: { incident: { name: '' }, comments: [] }, }; expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { - title: ['Name is required.'], + name: ['Name is required.'], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx index 251274a08ba6c..92b361897f8e3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx @@ -69,12 +69,15 @@ export function getActionType(): ActionTypeModel< validateParams: (actionParams: ResilientActionParams): ValidationResult => { const validationResult = { errors: {} }; const errors = { - title: new Array(), + name: new Array(), }; validationResult.errors = errors; - - if (!actionParams.subActionParams?.title?.length) { - errors.title.push(i18n.NAME_REQUIRED); + if ( + actionParams.subActionParams && + actionParams.subActionParams.incident && + !actionParams.subActionParams.incident.name?.length + ) { + errors.name.push(i18n.NAME_REQUIRED); } return validationResult; }, 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 a285c96219033..833f48fa50d9e 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 @@ -75,7 +75,6 @@ describe('ResilientActionConnectorFields renders', () => { /> ); - expect(wrapper.find('[data-test-subj="case-resilient-mappings"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); expect( wrapper.find('[data-test-subj="connector-resilient-orgId-form-input"]').length > 0 diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx index cf7596442a02b..25a5295e70432 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx @@ -17,25 +17,18 @@ import { EuiTitle, } from '@elastic/eui'; -import { isEmpty } from 'lodash'; import { ActionConnectorFieldsProps } from '../../../../types'; import * as i18n from './translations'; import { ResilientActionConnector } from './types'; -import { connectorConfiguration } from './config'; -import { FieldMapping, CasesConfigurationMapping, createDefaultMapping } from '../case_mappings'; const ResilientConnectorFields: React.FC> = ({ action, editActionSecrets, editActionConfig, errors, - consumer, readOnly, }) => { - // TODO: remove incidentConfiguration later, when Case Resilient will move their fields to the level of action execution - const { apiUrl, orgId, incidentConfiguration, isCaseOwned } = action.config; - const mapping = incidentConfiguration ? incidentConfiguration.mapping : []; - + const { apiUrl, orgId } = action.config; const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; const { apiKeyId, apiKeySecret } = action.secrets; @@ -44,39 +37,14 @@ const ResilientConnectorFields: React.FC 0 && apiKeyId != null; const isApiKeySecretInvalid: boolean = errors.apiKeySecret.length > 0 && apiKeySecret != null; - // TODO: remove this block later, when Case ServiceNow will move their fields to the level of action execution - if (consumer === 'case') { - if (isEmpty(mapping)) { - editActionConfig('incidentConfiguration', { - mapping: createDefaultMapping(connectorConfiguration.fields as any), - }); - } - - if (!isCaseOwned) { - editActionConfig('isCaseOwned', true); - } - } - const handleOnChangeActionConfig = useCallback( (key: string, value: string) => editActionConfig(key, value), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [editActionConfig] ); const handleOnChangeSecretConfig = useCallback( (key: string, value: string) => editActionSecrets(key, value), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const handleOnChangeMappingConfig = useCallback( - (newMapping: CasesConfigurationMapping[]) => - editActionConfig('incidentConfiguration', { - ...action.config.incidentConfiguration, - mapping: newMapping, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [action.config] + [editActionSecrets] ); return ( @@ -201,21 +169,6 @@ const ResilientConnectorFields: React.FC - {consumer === 'case' && ( // TODO: remove this block later, when Case Resilient will move their fields to the level of action execution - <> - - - - - - - - )} ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.test.tsx index 5a57006cdf112..d868764d625c0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.test.tsx @@ -20,13 +20,14 @@ const useGetSeverityMock = useGetSeverity as jest.Mock; const actionParams = { subAction: 'pushToService', subActionParams: { - title: 'title', - description: 'some description', + incident: { + name: 'title', + description: 'some description', + incidentTypes: [1001], + severityCode: 6, + externalId: null, + }, comments: [{ commentId: '1', comment: 'comment for resilient' }], - incidentTypes: [1001], - severityCode: 6, - savedObjectId: '123', - externalId: null, }, }; const connector = { @@ -80,7 +81,7 @@ describe('ResilientParamsFields renders', () => { const wrapper = mountWithIntl( {}} index={0} messageVariables={[{ name: AlertProvidedActionVariables.alertId, description: '' }]} @@ -91,35 +92,16 @@ describe('ResilientParamsFields renders', () => { expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('value')).toStrictEqual( 6 ); - expect(wrapper.find('[data-test-subj="titleInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="nameInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="descriptionTextArea"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="commentsTextArea"]').length > 0).toBeTruthy(); - - // ensure savedObjectIdInput isnt rendered - expect(wrapper.find('[data-test-subj="savedObjectIdInput"]').length === 0).toBeTruthy(); - }); - - test('the savedObjectId fields is rendered if we cant find an alertId in the messageVariables', () => { - const wrapper = mountWithIntl( - {}} - index={0} - messageVariables={[]} - actionConnector={connector} - /> - ); - - expect(wrapper.find('[data-test-subj="savedObjectIdInput"]').length > 0).toBeTruthy(); }); - test('it shows loading when loading incident types', () => { useGetIncidentTypesMock.mockReturnValue({ ...useGetIncidentTypesResponse, isLoading: true }); const wrapper = mountWithIntl( {}} index={0} messageVariables={[]} @@ -141,7 +123,7 @@ describe('ResilientParamsFields renders', () => { const wrapper = mountWithIntl( {}} index={0} messageVariables={[]} @@ -160,7 +142,7 @@ describe('ResilientParamsFields renders', () => { const wrapper = mountWithIntl( {}} index={0} messageVariables={[]} @@ -182,7 +164,7 @@ describe('ResilientParamsFields renders', () => { const wrapper = mountWithIntl( {}} index={0} messageVariables={[]} 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 8c384903b86e4..badb7479905d5 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useEffect, useState } from 'react'; +import React, { Fragment, useCallback, useEffect, useMemo, useRef } from 'react'; import { EuiFormRow, EuiComboBox, @@ -13,11 +13,8 @@ import { EuiTitle, EuiComboBoxOptionOption, EuiSelectOption, - EuiFormControlLayout, - EuiIconTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { isSome } from 'fp-ts/lib/Option'; import { ActionParamsProps } from '../../../../types'; import { ResilientActionParams } from './types'; @@ -26,44 +23,30 @@ import { TextFieldWithMessageVariables } from '../../text_field_with_message_var import { useGetIncidentTypes } from './use_get_incident_types'; import { useGetSeverity } from './use_get_severity'; -import { extractActionVariable } from '../extract_action_variable'; -import { AlertProvidedActionVariables } from '../../../lib/action_variables'; import { useKibana } from '../../../../common/lib/kibana'; const ResilientParamsFields: React.FunctionComponent> = ({ + actionConnector, actionParams, editAction, - index, errors, + index, messageVariables, - actionConnector, }) => { const { http, notifications: { toasts }, } = useKibana().services; - const [firstLoad, setFirstLoad] = useState(false); - const { title, description, comments, incidentTypes, severityCode, savedObjectId } = - actionParams.subActionParams || {}; - - const isActionBeingConfiguredByAnAlert = messageVariables - ? isSome(extractActionVariable(messageVariables, AlertProvidedActionVariables.alertId)) - : false; - - const [incidentTypesComboBoxOptions, setIncidentTypesComboBoxOptions] = useState< - Array> - >([]); - - const [selectedIncidentTypesComboBoxOptions, setSelectedIncidentTypesComboBoxOptions] = useState< - Array> - >([]); - - const [severitySelectOptions, setSeveritySelectOptions] = useState([]); - - useEffect(() => { - setFirstLoad(true); - }, []); - + const actionConnectorRef = useRef(actionConnector?.id ?? ''); + const { incident, comments } = useMemo( + () => + actionParams.subActionParams ?? + (({ + incident: {}, + comments: [], + } as unknown) as ResilientActionParams['subActionParams']), + [actionParams.subActionParams] + ); const { isLoading: isLoadingIncidentTypes, incidentTypes: allIncidentTypes, @@ -78,71 +61,107 @@ const ResilientParamsFields: React.FunctionComponent { - const newProps = { ...actionParams.subActionParams, [key]: value }; - editAction('subActionParams', newProps, index); - }; - - useEffect(() => { - const options = severity.map((s) => ({ + const severitySelectOptions: EuiSelectOption[] = useMemo(() => { + return severity.map((s) => ({ value: s.id.toString(), text: s.name, })); + }, [severity]); - setSeveritySelectOptions(options); - }, [actionConnector, severity]); - - // Reset parameters when changing connector - useEffect(() => { - if (!firstLoad) { - return; - } - - setIncidentTypesComboBoxOptions([]); - setSelectedIncidentTypesComboBoxOptions([]); - setSeveritySelectOptions([]); - editAction('subActionParams', { title, comments, description: '', savedObjectId }, index); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [actionConnector]); - - useEffect(() => { - if (!actionParams.subAction) { - editAction('subAction', 'pushToService', index); - } - if (!savedObjectId && isActionBeingConfiguredByAnAlert) { - editSubActionProperty('savedObjectId', `${AlertProvidedActionVariables.alertId}`); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [actionConnector, savedObjectId]); - - useEffect(() => { - setIncidentTypesComboBoxOptions( + const incidentTypesComboBoxOptions: Array> = useMemo( + () => allIncidentTypes ? allIncidentTypes.map((type: { id: number; name: string }) => ({ label: type.name, value: type.id.toString(), })) - : [] - ); - + : [], + [allIncidentTypes] + ); + const selectedIncidentTypesComboBoxOptions: Array< + EuiComboBoxOptionOption + > = useMemo(() => { const allIncidentTypesAsObject = allIncidentTypes.reduce( (acc, type) => ({ ...acc, [type.id.toString()]: type.name }), {} as Record ); + return incident.incidentTypes + ? incident.incidentTypes + .map((type) => ({ + label: allIncidentTypesAsObject[type.toString()], + value: type.toString(), + })) + .filter((type) => type.label != null) + : []; + }, [allIncidentTypes, incident.incidentTypes]); - setSelectedIncidentTypesComboBoxOptions( - incidentTypes - ? incidentTypes - .map((type) => ({ - label: allIncidentTypesAsObject[type.toString()], - value: type.toString(), - })) - .filter((type) => type.label != null) - : [] - ); + const editSubActionProperty = useCallback( + (key: string, value: any) => { + const newProps = + key !== 'comments' + ? { + incident: { ...incident, [key]: value }, + comments, + } + : { incident, [key]: value }; + editAction('subActionParams', newProps, index); + }, + [comments, editAction, incident, index] + ); + const editComment = useCallback( + (key, value) => { + if (value.length > 0) { + editSubActionProperty(key, [{ commentId: '1', comment: value }]); + } + }, + [editSubActionProperty] + ); + + const incidentTypesOnChange = useCallback( + (selectedOptions: Array<{ label: string; value?: string }>) => { + editSubActionProperty( + 'incidentTypes', + selectedOptions.map((selectedOption) => selectedOption.value ?? selectedOption.label) + ); + }, + [editSubActionProperty] + ); + const incidentTypesOnBlur = useCallback(() => { + if (!incident.incidentTypes) { + editSubActionProperty('incidentTypes', []); + } + }, [editSubActionProperty, incident.incidentTypes]); + + useEffect(() => { + if (actionConnector != null && actionConnectorRef.current !== actionConnector.id) { + actionConnectorRef.current = actionConnector.id; + editAction( + 'subActionParams', + { + incident: {}, + comments: [], + }, + index + ); + } + // 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 - }, [actionConnector, allIncidentTypes]); + }, [actionParams]); return ( @@ -154,9 +173,7 @@ const ResilientParamsFields: React.FunctionComponent ) => { - setSelectedIncidentTypesComboBoxOptions( - selectedOptions.map((selectedOption) => ({ - label: selectedOption.label, - value: selectedOption.value, - })) - ); - - editSubActionProperty( - 'incidentTypes', - selectedOptions.map((selectedOption) => selectedOption.value ?? selectedOption.label) - ); - }} - onBlur={() => { - if (!incidentTypes) { - editSubActionProperty('incidentTypes', []); - } - }} + onChange={incidentTypesOnChange} + onBlur={incidentTypesOnBlur} isClearable={true} /> @@ -192,108 +193,60 @@ const ResilientParamsFields: React.FunctionComponent editSubActionProperty('severityCode', e.target.value)} options={severitySelectOptions} - value={severityCode} - onChange={(e) => { - editSubActionProperty('severityCode', e.target.value); - }} + value={incident.severityCode ?? undefined} /> 0 && title !== undefined} + error={errors.name} + isInvalid={errors.name.length > 0 && incident.name !== undefined} label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.titleFieldLabel', - { - defaultMessage: 'Name', - } + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.nameFieldLabel', + { defaultMessage: 'Name (required)' } )} > - {!isActionBeingConfiguredByAnAlert && ( - - - - } - > - - - - - - )} { - editSubActionProperty(key, [{ commentId: 'alert-comment', comment: value }]); - }} + editAction={editComment} messageVariables={messageVariables} paramsProperty={'comments'} - inputTargetValue={comments && comments.length > 0 ? comments[0].comment : ''} + inputTargetValue={comments && comments.length > 0 ? comments[0].comment : undefined} label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.commentsTextAreaFieldLabel', - { - defaultMessage: 'Additional comments (optional)', - } + { defaultMessage: 'Additional comments' } )} errors={errors.comments as string[]} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/types.ts index 38019205fbfc9..d2ff5cd921652 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/types.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CasesConfigurationMapping } from '../case_mappings'; import { UserConfiguredActionConnector } from '../../../../types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ExecutorSubActionPushParams } from '../../../../../../actions/server/builtin_action_types/resilient/types'; export type ResilientActionConnector = UserConfiguredActionConnector< ResilientConfig, @@ -14,26 +15,12 @@ export type ResilientActionConnector = UserConfiguredActionConnector< export interface ResilientActionParams { subAction: string; - subActionParams: { - savedObjectId: string; - title: string; - description: string; - externalId: string | null; - incidentTypes: number[]; - severityCode: number; - comments: Array<{ commentId: string; comment: string }>; - }; -} - -interface IncidentConfiguration { - mapping: CasesConfigurationMapping[]; + subActionParams: ExecutorSubActionPushParams; } export interface ResilientConfig { apiUrl: string; orgId: string; - incidentConfiguration?: IncidentConfiguration; - isCaseOwned?: boolean; } export interface ResilientSecrets { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx index c29ddbf385de6..4f0c5e06e1428 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx @@ -75,22 +75,22 @@ describe('servicenow connector validation', () => { describe('servicenow action params validation', () => { test('action params validation succeeds when action params is valid', () => { const actionParams = { - subActionParams: { title: 'some title {{test}}' }, + subActionParams: { incident: { short_description: 'some title {{test}}' }, comments: [] }, }; expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { title: [] }, + errors: { short_description: [] }, }); }); test('params validation fails when body is not valid', () => { const actionParams = { - subActionParams: { title: '' }, + subActionParams: { incident: { short_description: '' }, comments: [] }, }; expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { - title: ['Short description is required.'], + short_description: ['Short description is required.'], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx index 8eca7f3ef3120..9e84034669483 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx @@ -64,11 +64,15 @@ export function getActionType(): ActionTypeModel< validateParams: (actionParams: ServiceNowActionParams): ValidationResult => { const validationResult = { errors: {} }; const errors = { - title: new Array(), + short_description: new Array(), }; validationResult.errors = errors; - if (!actionParams.subActionParams?.title?.length) { - errors.title.push(i18n.TITLE_REQUIRED); + if ( + actionParams.subActionParams && + actionParams.subActionParams.incident && + !actionParams.subActionParams.incident.short_description?.length + ) { + errors.short_description.push(i18n.TITLE_REQUIRED); } return validationResult; }, 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 de48e62d88aa1..61cfdd7bc8ee0 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 @@ -69,7 +69,6 @@ describe('ServiceNowActionConnectorFields renders', () => { consumer={'case'} /> ); - expect(wrapper.find('[data-test-subj="case-servicenow-mappings"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); expect( wrapper.find('[data-test-subj="connector-servicenow-password-form-input"]').length > 0 diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx index 328667ae49c69..4d37a9438fa16 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx @@ -18,24 +18,18 @@ import { EuiTitle, } from '@elastic/eui'; -import { isEmpty } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { ActionConnectorFieldsProps } from '../../../../types'; -import { CasesConfigurationMapping, FieldMapping, createDefaultMapping } from '../case_mappings'; import * as i18n from './translations'; import { ServiceNowActionConnector } from './types'; -import { connectorConfiguration } from './config'; import { useKibana } from '../../../../common/lib/kibana'; const ServiceNowConnectorFields: React.FC< ActionConnectorFieldsProps > = ({ action, editActionSecrets, editActionConfig, errors, consumer, readOnly }) => { const { docLinks } = useKibana().services; - - // TODO: remove incidentConfiguration later, when Case ServiceNow will move their fields to the level of action execution - const { apiUrl, incidentConfiguration, isCaseOwned } = action.config; - const mapping = incidentConfiguration ? incidentConfiguration.mapping : []; + const { apiUrl } = action.config; const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; @@ -44,40 +38,15 @@ const ServiceNowConnectorFields: React.FC< const isUsernameInvalid: boolean = errors.username.length > 0 && username != null; const isPasswordInvalid: boolean = errors.password.length > 0 && password != null; - // TODO: remove this block later, when Case ServiceNow will move their fields to the level of action execution - if (consumer === 'case') { - if (isEmpty(mapping)) { - editActionConfig('incidentConfiguration', { - mapping: createDefaultMapping(connectorConfiguration.fields as any), - }); - } - if (!isCaseOwned) { - editActionConfig('isCaseOwned', true); - } - } - const handleOnChangeActionConfig = useCallback( (key: string, value: string) => editActionConfig(key, value), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [editActionConfig] ); const handleOnChangeSecretConfig = useCallback( (key: string, value: string) => editActionSecrets(key, value), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [editActionSecrets] ); - - const handleOnChangeMappingConfig = useCallback( - (newMapping: CasesConfigurationMapping[]) => - editActionConfig('incidentConfiguration', { - ...action.config.incidentConfiguration, - mapping: newMapping, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [action.config] - ); - return ( <> @@ -185,21 +154,6 @@ const ServiceNowConnectorFields: React.FC< - {consumer === 'case' && ( // TODO: remove this block later, when Case ServiceNow will move their fields to the level of action execution - <> - - - - - - - - )} ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx index b3521b82abb38..eff754fd99bab 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx @@ -13,21 +13,23 @@ describe('ServiceNowParamsFields renders', () => { const actionParams = { subAction: 'pushToService', subActionParams: { - title: 'sn title', - description: 'some description', - comment: 'comment for sn', - severity: '1', - urgency: '2', - impact: '3', - savedObjectId: '123', - externalId: null, + incident: { + short_description: 'sn title', + description: 'some description', + severity: '1', + urgency: '2', + impact: '3', + savedObjectId: '123', + externalId: null, + }, + comments: [{ commentId: '1', comment: 'comment for sn' }], }, }; const wrapper = mountWithIntl( {}} index={0} messageVariables={[{ name: AlertProvidedActionVariables.alertId, description: '' }]} @@ -38,40 +40,8 @@ describe('ServiceNowParamsFields renders', () => { '1' ); expect(wrapper.find('[data-test-subj="impactSelect"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="titleInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="short_descriptionInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="descriptionTextArea"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="commentTextArea"]').length > 0).toBeTruthy(); - - // ensure savedObjectIdInput isnt rendered - expect(wrapper.find('[data-test-subj="savedObjectIdInput"]').length === 0).toBeTruthy(); - }); - - test('the savedObjectId fields is rendered if we cant find an alertId in the messageVariables', () => { - const actionParams = { - subAction: 'pushToService', - subActionParams: { - title: 'sn title', - description: 'some description', - comment: 'comment for sn', - severity: '1', - urgency: '2', - impact: '3', - savedObjectId: '123', - externalId: null, - }, - }; - - const wrapper = mountWithIntl( - {}} - index={0} - messageVariables={[]} - /> - ); - - // ensure savedObjectIdInput isnt rendered - expect(wrapper.find('[data-test-subj="savedObjectIdInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="commentsTextArea"]').length > 0).toBeTruthy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx index 240df24735414..f6e41ab8f35b1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useEffect } from 'react'; +import React, { Fragment, useCallback, useEffect, useMemo, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, @@ -13,80 +13,103 @@ import { EuiFlexItem, EuiSpacer, EuiTitle, - EuiFormControlLayout, - EuiIconTip, } from '@elastic/eui'; -import { isSome } from 'fp-ts/lib/Option'; import { ActionParamsProps } from '../../../../types'; import { ServiceNowActionParams } from './types'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; -import { extractActionVariable } from '../extract_action_variable'; -import { AlertProvidedActionVariables } from '../../../lib/action_variables'; + +const selectOptions = [ + { + value: '1', + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectHighOptionLabel', + { defaultMessage: 'High' } + ), + }, + { + value: '2', + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectMediumOptionLabel', + { defaultMessage: 'Medium' } + ), + }, + { + value: '3', + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectLawOptionLabel', + { defaultMessage: 'Low' } + ), + }, +]; const ServiceNowParamsFields: React.FunctionComponent< ActionParamsProps -> = ({ actionParams, editAction, index, errors, messageVariables }) => { - const { title, description, comment, severity, urgency, impact, savedObjectId } = - actionParams.subActionParams || {}; - - const isActionBeingConfiguredByAnAlert = messageVariables - ? isSome(extractActionVariable(messageVariables, AlertProvidedActionVariables.alertId)) - : false; +> = ({ actionConnector, actionParams, editAction, index, errors, messageVariables }) => { + const actionConnectorRef = useRef(actionConnector?.id ?? ''); + const { incident, comments } = useMemo( + () => + actionParams.subActionParams ?? + (({ + incident: {}, + comments: [], + } as unknown) as ServiceNowActionParams['subActionParams']), + [actionParams.subActionParams] + ); - const selectOptions = [ - { - value: '1', - text: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectHighOptionLabel', - { - defaultMessage: 'High', - } - ), - }, - { - value: '2', - text: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectMediumOptionLabel', - { - defaultMessage: 'Medium', - } - ), - }, - { - value: '3', - text: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectLawOptionLabel', - { - defaultMessage: 'Low', - } - ), + const editSubActionProperty = useCallback( + (key: string, value: any) => { + const newProps = + key !== 'comments' + ? { + incident: { ...incident, [key]: value }, + comments, + } + : { incident, [key]: value }; + editAction('subActionParams', newProps, index); }, - ]; + [comments, editAction, incident, index] + ); - const editSubActionProperty = (key: string, value: {}) => { - const newProps = { ...actionParams.subActionParams, [key]: value }; - editAction('subActionParams', newProps, index); - }; + const editComment = useCallback( + (key, value) => { + if (value.length > 0) { + editSubActionProperty(key, [{ commentId: '1', comment: value }]); + } + }, + [editSubActionProperty] + ); + useEffect(() => { + if (actionConnector != null && actionConnectorRef.current !== actionConnector.id) { + actionConnectorRef.current = actionConnector.id; + editAction( + 'subActionParams', + { + incident: {}, + comments: [], + }, + index + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionConnector]); useEffect(() => { if (!actionParams.subAction) { editAction('subAction', 'pushToService', index); } - if (!savedObjectId && isActionBeingConfiguredByAnAlert) { - editSubActionProperty('savedObjectId', `${AlertProvidedActionVariables.alertId}`); - } - if (!urgency) { - editSubActionProperty('urgency', '3'); - } - if (!impact) { - editSubActionProperty('impact', '3'); - } - if (!severity) { - editSubActionProperty('severity', '3'); + if (!actionParams.subActionParams) { + editAction( + 'subActionParams', + { + incident: {}, + comments: [], + }, + index + ); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [title, description, comment, severity, impact, urgency]); + }, [actionParams]); return ( @@ -94,9 +117,7 @@ const ServiceNowParamsFields: React.FunctionComponent<

{i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.title', - { - defaultMessage: 'Incident', - } + { defaultMessage: 'Incident' } )}

@@ -105,19 +126,16 @@ const ServiceNowParamsFields: React.FunctionComponent< fullWidth label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.urgencySelectFieldLabel', - { - defaultMessage: 'Urgency', - } + { defaultMessage: 'Urgency' } )} > { - editSubActionProperty('urgency', e.target.value); - }} + value={incident.urgency ?? undefined} + onChange={(e) => editSubActionProperty('urgency', e.target.value)} /> @@ -127,19 +145,16 @@ const ServiceNowParamsFields: React.FunctionComponent< fullWidth label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.severitySelectFieldLabel', - { - defaultMessage: 'Severity', - } + { defaultMessage: 'Severity' } )} > { - editSubActionProperty('severity', e.target.value); - }} + value={incident.severity ?? undefined} + onChange={(e) => editSubActionProperty('severity', e.target.value)} /> @@ -148,19 +163,16 @@ const ServiceNowParamsFields: React.FunctionComponent< fullWidth label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.impactSelectFieldLabel', - { - defaultMessage: 'Impact', - } + { defaultMessage: 'Impact' } )} > { - editSubActionProperty('impact', e.target.value); - }} + value={incident.impact ?? undefined} + onChange={(e) => editSubActionProperty('impact', e.target.value)} /> @@ -168,88 +180,45 @@ const ServiceNowParamsFields: React.FunctionComponent< 0 && title !== undefined} + error={errors.short_description} + isInvalid={errors.short_description.length > 0 && incident.short_description !== undefined} label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.titleFieldLabel', - { - defaultMessage: 'Short description', - } + { defaultMessage: 'Short description (required)' } )} > - {!isActionBeingConfiguredByAnAlert && ( - - - - } - > - - - - - - )} 0 ? comments[0].comment : undefined} label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.commentsTextAreaFieldLabel', - { - defaultMessage: 'Additional comments (optional)', - } + { defaultMessage: 'Additional comments' } )} - errors={errors.comment as string[]} + errors={errors.comments as string[]} />
); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts index 91a5c0a54397b..c84a916c0fef4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts @@ -155,7 +155,7 @@ export const DESCRIPTION_REQUIRED = i18n.translate( ); export const TITLE_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredTitleTextField', + 'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredShortDescTextField', { defaultMessage: 'Short description is required.', } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts index 92753dfcba76c..ae03680a80534 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CasesConfigurationMapping } from '../case_mappings'; import { UserConfiguredActionConnector } from '../../../../types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ExecutorSubActionPushParams } from '../../../../../../actions/server/builtin_action_types/servicenow/types'; export type ServiceNowActionConnector = UserConfiguredActionConnector< ServiceNowConfig, @@ -14,26 +15,11 @@ export type ServiceNowActionConnector = UserConfiguredActionConnector< export interface ServiceNowActionParams { subAction: string; - subActionParams: { - savedObjectId: string; - title: string; - description: string; - comment: string; - externalId: string | null; - severity: string; - urgency: string; - impact: string; - }; -} - -interface IncidentConfiguration { - mapping: CasesConfigurationMapping[]; + subActionParams: ExecutorSubActionPushParams; } export interface ServiceNowConfig { apiUrl: string; - incidentConfiguration?: IncidentConfiguration; - isCaseOwned?: boolean; } export interface ServiceNowSecrets { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 9de3ae21a8ef7..d7d508cbcf121 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -174,9 +174,7 @@ describe('action_form', () => { id: '.servicenow', actionTypeId: '.servicenow', name: 'Non consumer connector', - config: { - isCaseOwned: true, - }, + config: {}, isPreconfigured: false, }, { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index 3cf6e18e89621..116944ce0ca2b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -85,6 +85,19 @@ export const ConnectorEditFlyout = ({ Option> >(none); const [isExecutingAction, setIsExecutinAction] = useState(false); + const handleSetTab = useCallback( + () => + setTab((prevTab) => { + if (prevTab === EditConectorTabs.Configuration) { + return EditConectorTabs.Test; + } + if (testExecutionResult !== none) { + setTestExecutionResult(none); + } + return EditConectorTabs.Configuration; + }), + [testExecutionResult] + ); const closeFlyout = useCallback(() => { setConnector('connector', { ...initialConnector, secrets: {} }); @@ -223,7 +236,7 @@ export const ConnectorEditFlyout = ({ setTab(EditConectorTabs.Configuration)} + onClick={handleSetTab} data-test-subj="configureConnectorTab" isSelected={EditConectorTabs.Configuration === selectedTab} > @@ -232,7 +245,7 @@ export const ConnectorEditFlyout = ({ })} setTab(EditConectorTabs.Test)} + onClick={handleSetTab} data-test-subj="testConnectorTab" isSelected={EditConectorTabs.Test === selectedTab} > diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.ts index b86e0d1555315..0e9eb474e10ac 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.ts @@ -152,7 +152,11 @@ export const alertReducer = ( keyof AlertAction, SavedObjectAttribute >; - if (index === undefined || isEqual(alert.actions[index][key], value)) { + if ( + index === undefined || + alert.actions[index] == null || + isEqual(alert.actions[index][key], value) + ) { return state; } else { const oldAction = alert.actions.splice(index, 1)[0]; diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts index 025fd558ee1ca..a208d0ab22d62 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts @@ -11,26 +11,6 @@ import { ExternalServiceSimulator, } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; -// node ../scripts/functional_test_runner.js --grep "Actions.servicenddd" --config=test/alerting_api_integration/security_and_spaces/config.ts - -const mapping = [ - { - source: 'title', - target: 'summary', - actionType: 'nothing', - }, - { - source: 'description', - target: 'description', - actionType: 'nothing', - }, - { - source: 'comments', - target: 'comments', - actionType: 'nothing', - }, -]; - // eslint-disable-next-line import/no-default-export export default function jiraTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -38,21 +18,20 @@ export default function jiraTest({ getService }: FtrProviderContext) { const mockJira = { config: { apiUrl: 'www.jiraisinkibanaactions.com', - incidentConfiguration: { mapping: [...mapping] }, - isCaseOwned: true, }, secrets: { email: 'elastic', apiToken: 'changeme', }, params: { - savedObjectId: '123', - title: 'a title', - description: 'a description', - labels: ['kibana'], - issueType: '10006', - priority: 'High', - externalId: null, + incident: { + summary: 'a title', + description: 'a description', + labels: ['kibana'], + issueType: '10006', + priority: 'High', + externalId: null, + }, comments: [ { commentId: '456', @@ -81,8 +60,6 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: jiraSimulatorURL, projectKey: 'CK', - incidentConfiguration: { ...mockJira.config.incidentConfiguration }, - isCaseOwned: true, }, secrets: mockJira.secrets, }) diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/resilient.ts index 576ed4bbc5dfe..7576d4ac4c28f 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/resilient.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/resilient.ts @@ -11,24 +11,6 @@ import { ExternalServiceSimulator, } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; -const mapping = [ - { - source: 'title', - target: 'description', - actionType: 'nothing', - }, - { - source: 'description', - target: 'short_description', - actionType: 'nothing', - }, - { - source: 'comments', - target: 'comments', - actionType: 'nothing', - }, -]; - // eslint-disable-next-line import/no-default-export export default function resilientTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -37,19 +19,18 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: 'www.resilientisinkibanaactions.com', orgId: '201', - incidentConfiguration: { mapping: [...mapping] }, - isCaseOwned: true, }, secrets: { apiKeyId: 'elastic', apiKeySecret: 'changeme', }, params: { - savedObjectId: '123', - title: 'a title', - description: 'a description', - incidentTypes: [1001], - severityCode: 'High', + incident: { + name: 'a title', + description: 'a description', + incidentTypes: [1001], + severityCode: 'High', + }, comments: [ { commentId: '456', @@ -77,8 +58,6 @@ export default function resilientTest({ getService }: FtrProviderContext) { actionTypeId: '.resilient', config: { apiUrl: resilientSimulatorURL, - incidentConfiguration: { ...mockResilient.config.incidentConfiguration }, - isCaseOwned: true, }, secrets: mockResilient.secrets, }) diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts index a451edea76d83..a2c2fffed4ea0 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts @@ -11,26 +11,6 @@ import { ExternalServiceSimulator, } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; -// node ../scripts/functional_test_runner.js --grep "Actions.servicenddd" --config=test/alerting_api_integration/security_and_spaces/config.ts - -const mapping = [ - { - source: 'title', - target: 'description', - actionType: 'nothing', - }, - { - source: 'description', - target: 'short_description', - actionType: 'nothing', - }, - { - source: 'comments', - target: 'comments', - actionType: 'nothing', - }, -]; - // eslint-disable-next-line import/no-default-export export default function servicenowTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -38,21 +18,19 @@ export default function servicenowTest({ getService }: FtrProviderContext) { const mockServiceNow = { config: { apiUrl: 'www.servicenowisinkibanaactions.com', - incidentConfiguration: { mapping: [...mapping] }, - isCaseOwned: true, }, secrets: { password: 'elastic', username: 'changeme', }, params: { - savedObjectId: '123', - title: 'a title', - description: 'a description', - comment: 'test-alert comment', - severity: '1', - urgency: '2', - impact: '1', + incident: { + short_description: 'a title', + description: 'a description', + severity: '1', + urgency: '2', + impact: '1', + }, comments: [ { commentId: '456', @@ -80,8 +58,6 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - incidentConfiguration: { ...mockServiceNow.config.incidentConfiguration }, - isCaseOwned: true, }, secrets: mockServiceNow.secrets, }) 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 43e4f642bb943..e7ce0638c6319 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 @@ -37,6 +37,9 @@ export function getAllExternalServiceSimulatorPaths(): string[] { getExternalServiceSimulatorPath(service) ); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/incident`); + allPaths.push( + `/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/sys_dictionary` + ); 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`); diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/servicenow_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/servicenow_simulation.ts index e2f31da1c8064..2c3138a36f071 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/servicenow_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/servicenow_simulation.ts @@ -89,6 +89,44 @@ export function initPlugin(router: IRouter, path: string) { }); } ); + + router.get( + { + path: `${path}/api/now/v2/table/sys_dictionary`, + options: { + authRequired: false, + }, + validate: {}, + }, + async function ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + return jsonResponse(res, 200, { + result: [ + { + column_label: 'Close notes', + mandatory: 'false', + max_length: '4000', + element: 'close_notes', + }, + { + column_label: 'Description', + mandatory: 'false', + max_length: '4000', + element: 'description', + }, + { + column_label: 'Short description', + mandatory: 'false', + max_length: '160', + element: 'short_description', + }, + ], + }); + } + ); } function jsonResponse(res: KibanaResponseFactory, code: number, object?: Record) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index edac71b8c594f..aba2b8426adf1 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -15,24 +15,6 @@ import { ExternalServiceSimulator, } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; -const mapping = [ - { - source: 'title', - target: 'summary', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, -]; - // eslint-disable-next-line import/no-default-export export default function jiraTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -43,7 +25,6 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: 'www.jiraisinkibanaactions.com', projectKey: 'CK', - incidentConfiguration: { mapping }, }, secrets: { apiToken: 'elastic', @@ -52,23 +33,15 @@ export default function jiraTest({ getService }: FtrProviderContext) { params: { subAction: 'pushToService', subActionParams: { - savedObjectId: '123', - title: 'a title', - description: 'a description', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - externalId: null, + incident: { + summary: 'a title', + description: 'a description', + externalId: null, + }, comments: [ { - commentId: '456', - version: 'WzU3LDFd', comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, + commentId: '456', }, ], }, @@ -94,8 +67,6 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { ...mockJira.config, apiUrl: jiraSimulatorURL, - incidentConfiguration: mockJira.config.incidentConfiguration, - isCaseOwned: true, }, secrets: mockJira.secrets, }) @@ -109,8 +80,6 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, - incidentConfiguration: mockJira.config.incidentConfiguration, - isCaseOwned: true, }, }); @@ -126,8 +95,6 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, - incidentConfiguration: mockJira.config.incidentConfiguration, - isCaseOwned: true, }, }); }); @@ -182,7 +149,6 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: 'http://jira.mynonexistent.com', projectKey: mockJira.config.projectKey, - incidentConfiguration: mockJira.config.incidentConfiguration, }, secrets: mockJira.secrets, }) @@ -207,7 +173,6 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, - incidentConfiguration: mockJira.config.incidentConfiguration, }, }) .expect(400) @@ -220,56 +185,6 @@ export default function jiraTest({ getService }: FtrProviderContext) { }); }); }); - - it('should respond with a 400 Bad Request when creating a jira action with empty mapping', async () => { - await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A jira action', - actionTypeId: '.jira', - config: { - apiUrl: jiraSimulatorURL, - projectKey: mockJira.config.projectKey, - incidentConfiguration: { mapping: [] }, - }, - secrets: mockJira.secrets, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: [incidentConfiguration.mapping]: expected non-empty but got empty', - }); - }); - }); - - it('should respond with a 400 Bad Request when creating a jira action with wrong actionType', async () => { - await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A jira action', - actionTypeId: '.jira', - config: { - apiUrl: jiraSimulatorURL, - projectKey: mockJira.config.projectKey, - incidentConfiguration: { - mapping: [ - { - source: 'title', - target: 'description', - actionType: 'non-supported', - }, - ], - }, - }, - secrets: mockJira.secrets, - }) - .expect(400); - }); }); describe('Jira - Executor', () => { @@ -287,7 +202,6 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, - incidentConfiguration: mockJira.config.incidentConfiguration, }, secrets: mockJira.secrets, }); @@ -375,7 +289,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.title]: expected value of type [string] but got [undefined]\n- [4.subAction]: expected value to equal [issueTypes]\n- [5.subAction]: expected value to equal [fieldsByIssueType]\n- [6.subAction]: expected value to equal [issues]\n- [7.subAction]: expected value to equal [issue]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.summary]: expected value of type [string] but got [undefined]\n- [4.subAction]: expected value to equal [issueTypes]\n- [5.subAction]: expected value to equal [fieldsByIssueType]\n- [6.subAction]: expected value to equal [issues]\n- [7.subAction]: expected value to equal [issue]', }); }); }); @@ -388,7 +302,10 @@ export default function jiraTest({ getService }: FtrProviderContext) { params: { ...mockJira.params, subActionParams: { - savedObjectId: 'success', + incident: { + description: 'success', + }, + comments: [], }, }, }) @@ -398,7 +315,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.title]: expected value of type [string] but got [undefined]\n- [4.subAction]: expected value to equal [issueTypes]\n- [5.subAction]: expected value to equal [fieldsByIssueType]\n- [6.subAction]: expected value to equal [issues]\n- [7.subAction]: expected value to equal [issue]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.summary]: expected value of type [string] but got [undefined]\n- [4.subAction]: expected value to equal [issueTypes]\n- [5.subAction]: expected value to equal [fieldsByIssueType]\n- [6.subAction]: expected value to equal [issues]\n- [7.subAction]: expected value to equal [issue]', }); }); }); @@ -411,12 +328,12 @@ export default function jiraTest({ getService }: FtrProviderContext) { params: { ...mockJira.params, subActionParams: { - ...mockJira.params.subActionParams, - savedObjectId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{}], + incident: { + ...mockJira.params.subActionParams.incident, + description: 'success', + summary: 'success', + }, + comments: [{ comment: 'comment' }], }, }, }) @@ -439,11 +356,10 @@ export default function jiraTest({ getService }: FtrProviderContext) { params: { ...mockJira.params, subActionParams: { - ...mockJira.params.subActionParams, - savedObjectId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, + incident: { + ...mockJira.params.subActionParams.incident, + summary: 'success', + }, comments: [{ commentId: 'success' }], }, }, @@ -469,9 +385,11 @@ export default function jiraTest({ getService }: FtrProviderContext) { params: { ...mockJira.params, subActionParams: { - ...mockJira.params.subActionParams, + incident: { + ...mockJira.params.subActionParams.incident, + issueType: '10006', + }, comments: [], - issueType: '10006', }, }, }) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts index 617f66ec98f50..392a430134352 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts @@ -15,24 +15,6 @@ import { ExternalServiceSimulator, } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; -const mapping = [ - { - source: 'title', - target: 'name', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, -]; - // eslint-disable-next-line import/no-default-export export default function resilientTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -43,8 +25,6 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: 'www.resilientisinkibanaactions.com', orgId: '201', - incidentConfiguration: { mapping }, - isCaseOwned: true, }, secrets: { apiKeyId: 'key', @@ -53,25 +33,17 @@ export default function resilientTest({ getService }: FtrProviderContext) { params: { subAction: 'pushToService', subActionParams: { - savedObjectId: '123', - title: 'a title', - description: 'a description', - incidentTypes: [1001], - severityCode: 6, - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - externalId: null, + incident: { + name: 'a title', + description: 'a description', + incidentTypes: [1001], + severityCode: 6, + externalId: null, + }, comments: [ { - commentId: '456', - version: 'WzU3LDFd', comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, + commentId: '456', }, ], }, @@ -111,8 +83,6 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, - incidentConfiguration: mockResilient.config.incidentConfiguration, - isCaseOwned: true, }, }); @@ -128,8 +98,6 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, - incidentConfiguration: mockResilient.config.incidentConfiguration, - isCaseOwned: true, }, }); }); @@ -184,7 +152,6 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: 'http://resilient.mynonexistent.com', orgId: mockResilient.config.orgId, - incidentConfiguration: mockResilient.config.incidentConfiguration, }, secrets: mockResilient.secrets, }) @@ -209,7 +176,6 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, - incidentConfiguration: mockResilient.config.incidentConfiguration, }, }) .expect(400) @@ -222,56 +188,6 @@ export default function resilientTest({ getService }: FtrProviderContext) { }); }); }); - - it('should respond with a 400 Bad Request when creating a ibm resilient action with empty mapping', async () => { - await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'An IBM Resilient', - actionTypeId: '.resilient', - config: { - apiUrl: resilientSimulatorURL, - orgId: mockResilient.config.orgId, - incidentConfiguration: { mapping: [] }, - }, - secrets: mockResilient.secrets, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: [incidentConfiguration.mapping]: expected non-empty but got empty', - }); - }); - }); - - it('should respond with a 400 Bad Request when creating a ibm resilient action with wrong actionType', async () => { - await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'An IBM Resilient', - actionTypeId: '.resilient', - config: { - apiUrl: resilientSimulatorURL, - orgId: mockResilient.config.orgId, - incidentConfiguration: { - mapping: [ - { - source: 'title', - target: 'description', - actionType: 'non-supported', - }, - ], - }, - }, - secrets: mockResilient.secrets, - }) - .expect(400); - }); }); describe('IBM Resilient - Executor', () => { @@ -288,7 +204,6 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, - incidentConfiguration: mockResilient.config.incidentConfiguration, }, secrets: mockResilient.secrets, }); @@ -376,7 +291,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.title]: expected value of type [string] but got [undefined]\n- [4.subAction]: expected value to equal [incidentTypes]\n- [5.subAction]: expected value to equal [severity]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.name]: expected value of type [string] but got [undefined]\n- [4.subAction]: expected value to equal [incidentTypes]\n- [5.subAction]: expected value to equal [severity]', }); }); }); @@ -389,7 +304,10 @@ export default function resilientTest({ getService }: FtrProviderContext) { params: { ...mockResilient.params, subActionParams: { - savedObjectId: 'success', + incident: { + description: 'success', + }, + comments: [], }, }, }) @@ -399,7 +317,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.title]: expected value of type [string] but got [undefined]\n- [4.subAction]: expected value to equal [incidentTypes]\n- [5.subAction]: expected value to equal [severity]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.name]: expected value of type [string] but got [undefined]\n- [4.subAction]: expected value to equal [incidentTypes]\n- [5.subAction]: expected value to equal [severity]', }); }); }); @@ -412,12 +330,11 @@ export default function resilientTest({ getService }: FtrProviderContext) { params: { ...mockResilient.params, subActionParams: { - ...mockResilient.params.subActionParams, - savedObjectId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{}], + incident: { + ...mockResilient.params.subActionParams.incident, + name: 'success', + }, + comments: [{ comment: 'comment' }], }, }, }) @@ -440,11 +357,10 @@ export default function resilientTest({ getService }: FtrProviderContext) { params: { ...mockResilient.params, subActionParams: { - ...mockResilient.params.subActionParams, - savedObjectId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, + incident: { + ...mockResilient.params.subActionParams.incident, + name: 'success', + }, comments: [{ commentId: 'success' }], }, }, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 47bfd3c496123..e448ad1f9c2ad 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -15,24 +15,6 @@ import { ExternalServiceSimulator, } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; -const mapping = [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, -]; - // eslint-disable-next-line import/no-default-export export default function servicenowTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -42,8 +24,6 @@ export default function servicenowTest({ getService }: FtrProviderContext) { const mockServiceNow = { config: { apiUrl: 'www.servicenowisinkibanaactions.com', - incidentConfiguration: { mapping }, - isCaseOwned: true, }, secrets: { password: 'elastic', @@ -52,27 +32,20 @@ export default function servicenowTest({ getService }: FtrProviderContext) { params: { subAction: 'pushToService', subActionParams: { - savedObjectId: '123', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, + incident: { + description: 'a description', + externalId: null, + impact: '1', + severity: '1', + short_description: 'a title', + urgency: '1', + }, comments: [ { - commentId: '456', comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, + commentId: '456', }, ], - description: 'a description', - externalId: null, - title: 'a title', - severity: '1', - urgency: '1', - impact: '1', - updatedAt: '2020-06-17T04:37:45.147Z', - updatedBy: { fullName: null, username: 'elastic' }, }, }, }; @@ -96,8 +69,6 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - incidentConfiguration: mockServiceNow.config.incidentConfiguration, - isCaseOwned: true, }, secrets: mockServiceNow.secrets, }) @@ -110,8 +81,6 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - incidentConfiguration: mockServiceNow.config.incidentConfiguration, - isCaseOwned: true, }, }); @@ -126,8 +95,6 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - incidentConfiguration: mockServiceNow.config.incidentConfiguration, - isCaseOwned: true, }, }); }); @@ -161,8 +128,6 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: 'http://servicenow.mynonexistent.com', - incidentConfiguration: mockServiceNow.config.incidentConfiguration, - isCaseOwned: true, }, secrets: mockServiceNow.secrets, }) @@ -186,8 +151,6 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - incidentConfiguration: mockServiceNow.config.incidentConfiguration, - isCaseOwned: true, }, }) .expect(400) @@ -200,72 +163,6 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }); }); }); - - it('should create a servicenow action without incidentConfiguration', async () => { - await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - isCaseOwned: true, - }, - secrets: mockServiceNow.secrets, - }) - .expect(200); - }); - - it('should respond with a 400 Bad Request when creating a servicenow action with empty mapping', async () => { - await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - incidentConfiguration: { mapping: [] }, - isCaseOwned: true, - }, - secrets: mockServiceNow.secrets, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: [incidentConfiguration.mapping]: expected non-empty but got empty', - }); - }); - }); - - it('should respond with a 400 Bad Request when creating a servicenow action with wrong actionType', async () => { - await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - incidentConfiguration: { - mapping: [ - { - source: 'title', - target: 'description', - actionType: 'non-supported', - }, - ], - }, - isCaseOwned: true, - }, - secrets: mockServiceNow.secrets, - }) - .expect(400); - }); }); describe('ServiceNow - Executor', () => { @@ -281,8 +178,6 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - incidentConfiguration: mockServiceNow.config.incidentConfiguration, - isCaseOwned: true, }, secrets: mockServiceNow.secrets, }); @@ -370,7 +265,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.title]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.short_description]: expected value of type [string] but got [undefined]', }); }); }); @@ -393,7 +288,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.title]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.short_description]: expected value of type [string] but got [undefined]', }); }); }); @@ -406,12 +301,11 @@ export default function servicenowTest({ getService }: FtrProviderContext) { params: { ...mockServiceNow.params, subActionParams: { - ...mockServiceNow.params.subActionParams, - savedObjectId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{}], + incident: { + ...mockServiceNow.params.subActionParams.incident, + short_description: 'success', + }, + comments: [{ comment: 'boo' }], }, }, }) @@ -421,7 +315,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.comments.0.commentId]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.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]', }); }); }); @@ -434,11 +328,10 @@ export default function servicenowTest({ getService }: FtrProviderContext) { params: { ...mockServiceNow.params, subActionParams: { - ...mockServiceNow.params.subActionParams, - savedObjectId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, + incident: { + ...mockServiceNow.params.subActionParams.incident, + short_description: 'success', + }, comments: [{ commentId: 'success' }], }, }, @@ -449,7 +342,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.comments.0.comment]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.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]', }); }); }); @@ -464,7 +357,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { params: { ...mockServiceNow.params, subActionParams: { - ...mockServiceNow.params.subActionParams, + incident: mockServiceNow.params.subActionParams.incident, comments: [], }, }, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts index d46d60905da1c..214c161932f48 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts @@ -22,36 +22,18 @@ export default function createGetTests({ getService }: FtrProviderContext) { await esArchiver.unload('actions'); }); - it('7.10.0 migrates the `casesConfiguration` to be the `incidentConfiguration` in `config`', async () => { + it('7.10.0 migrates the `casesConfiguration` to be the `incidentConfiguration` in `config`, then 7.11.0 removes `incidentConfiguration`', async () => { const response = await supertest.get( `${getUrlPrefix(``)}/api/actions/action/791a2ab1-784a-46ea-aa68-04c837e5da2d` ); expect(response.status).to.eql(200); - expect(response.body.config).key('incidentConfiguration'); + expect(response.body.config).not.key('incidentConfiguration'); expect(response.body.config).not.key('casesConfiguration'); + expect(response.body.config).not.key('isCaseOwned'); expect(response.body.config).to.eql({ apiUrl: 'http://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/jira', - incidentConfiguration: { - mapping: [ - { - actionType: 'overwrite', - source: 'title', - target: 'summary', - }, - { - actionType: 'overwrite', - source: 'description', - target: 'description', - }, - { - actionType: 'append', - source: 'comments', - target: 'comments', - }, - ], - }, projectKey: 'CK', }); }); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts index 3cf0d6892377e..906033e1ddc45 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts @@ -18,15 +18,27 @@ import { getConfiguration, getServiceNowConnector, } from '../../../common/lib/utils'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); const es = getService('es'); describe('push_case', () => { const actionsRemover = new ActionsRemover(supertest); + let servicenowSimulatorURL: string = ''; + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + afterEach(async () => { await deleteCases(es); await deleteComments(es); @@ -39,11 +51,13 @@ export default ({ getService }: FtrProviderContext): void => { const { body: connector } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') - .send(getServiceNowConnector()) + .send({ + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }) .expect(200); actionsRemover.add('default', connector.id, 'action', 'actions'); - const { body: configure } = await supertest .post(CASE_CONFIGURE_URL) .set('kbn-xsrf', 'true') @@ -55,7 +69,6 @@ export default ({ getService }: FtrProviderContext): void => { }) ) .expect(200); - const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') @@ -90,7 +103,10 @@ export default ({ getService }: FtrProviderContext): void => { const { body: connector } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') - .send(getServiceNowConnector()) + .send({ + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }) .expect(200); actionsRemover.add('default', connector.id, 'action', 'actions'); @@ -98,7 +114,13 @@ export default ({ getService }: FtrProviderContext): void => { const { body: configure } = await supertest .post(CASE_CONFIGURE_URL) .set('kbn-xsrf', 'true') - .send(getConfiguration(connector.id)) + .send( + getConfiguration({ + id: connector.id, + name: connector.name, + type: connector.actionTypeId, + }) + ) .expect(200); const { body: postedCase } = await supertest diff --git a/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts index ec79c8a1ca494..4eb87d2c2d2ce 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts @@ -20,14 +20,25 @@ import { } from '../../../../common/lib/utils'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); const actionsRemover = new ActionsRemover(supertest); + const kibanaServer = getService('kibanaServer'); describe('get_all_user_actions', () => { + let servicenowSimulatorURL: string = ''; + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); afterEach(async () => { await deleteCases(es); await deleteComments(es); @@ -318,7 +329,10 @@ export default ({ getService }: FtrProviderContext): void => { const { body: connector } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') - .send(getServiceNowConnector()) + .send({ + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }) .expect(200); actionsRemover.add('default', connector.id, 'action', 'actions'); diff --git a/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts b/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts index 5195d28d84830..d55aca1780c86 100644 --- a/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts @@ -13,8 +13,6 @@ import { getServiceNowConnector, getJiraConnector, getResilientConnector, - getConnectorWithoutCaseOwned, - getConnectorWithoutMapping, } from '../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export @@ -73,63 +71,17 @@ export default ({ getService }: FtrProviderContext): void => { .send(getResilientConnector()) .expect(200); - const { body: connectorWithoutCaseOwned } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send(getConnectorWithoutCaseOwned()) - .expect(200); - - const { body: connectorNoMapping } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send(getConnectorWithoutMapping()) - .expect(200); - actionsRemover.add('default', snConnector.id, 'action', 'actions'); actionsRemover.add('default', emailConnector.id, 'action', 'actions'); actionsRemover.add('default', jiraConnector.id, 'action', 'actions'); actionsRemover.add('default', resilientConnector.id, 'action', 'actions'); - actionsRemover.add('default', connectorWithoutCaseOwned.id, 'action', 'actions'); - actionsRemover.add('default', connectorNoMapping.id, 'action', 'actions'); const { body: connectors } = await supertest .get(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`) .set('kbn-xsrf', 'true') .send() .expect(200); - expect(connectors).to.eql([ - { - id: connectorWithoutCaseOwned.id, - actionTypeId: '.resilient', - name: 'Connector without isCaseOwned', - config: { - apiUrl: 'http://some.non.existent.com', - orgId: 'pkey', - incidentConfiguration: { - mapping: [ - { - source: 'title', - target: 'name', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, - isCaseOwned: null, - }, - isPreconfigured: false, - referencedByCount: 0, - }, { id: jiraConnector.id, actionTypeId: '.jira', @@ -137,26 +89,6 @@ export default ({ getService }: FtrProviderContext): void => { config: { apiUrl: 'http://some.non.existent.com', projectKey: 'pkey', - incidentConfiguration: { - mapping: [ - { - source: 'title', - target: 'summary', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, - isCaseOwned: true, }, isPreconfigured: false, referencedByCount: 0, @@ -168,26 +100,6 @@ export default ({ getService }: FtrProviderContext): void => { config: { apiUrl: 'http://some.non.existent.com', orgId: 'pkey', - incidentConfiguration: { - mapping: [ - { - source: 'title', - target: 'name', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, - isCaseOwned: true, }, isPreconfigured: false, referencedByCount: 0, @@ -198,26 +110,6 @@ export default ({ getService }: FtrProviderContext): void => { name: 'ServiceNow Connector', config: { apiUrl: 'http://some.non.existent.com', - incidentConfiguration: { - mapping: [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'append', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, - isCaseOwned: true, }, isPreconfigured: false, referencedByCount: 0, diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 86d69266c6ec6..ee054508b7491 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -7,7 +7,10 @@ import { CA_CERT_PATH } from '@kbn/dev-utils'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import path from 'path'; +import fs from 'fs'; import { services } from './services'; +import { getAllExternalServiceSimulatorPaths } from '../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; interface CreateTestConfigOptions { license: string; @@ -50,6 +53,34 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) }, }; + const allFiles = fs.readdirSync( + path.resolve( + __dirname, + '..', + '..', + 'alerting_api_integration', + 'common', + 'fixtures', + 'plugins' + ) + ); + const plugins = allFiles.filter((file) => + fs + .statSync( + path.resolve( + __dirname, + '..', + '..', + 'alerting_api_integration', + 'common', + 'fixtures', + 'plugins', + file + ) + ) + .isDirectory() + ); + return { testFiles: [require.resolve(`../${name}/tests/`)], servers, @@ -77,6 +108,20 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, '--xpack.eventLog.logEntries=true', ...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`), + ...plugins.map( + (pluginDir) => + `--plugin-path=${path.resolve( + __dirname, + '..', + '..', + 'alerting_api_integration', + 'common', + 'fixtures', + 'plugins', + pluginDir + )}` + ), + `--server.xsrf.whitelist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, ...(ssl ? [ `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, diff --git a/x-pack/test/case_api_integration/common/lib/mock.ts b/x-pack/test/case_api_integration/common/lib/mock.ts index 012af6b37f842..d61b999a745a0 100644 --- a/x-pack/test/case_api_integration/common/lib/mock.ts +++ b/x-pack/test/case_api_integration/common/lib/mock.ts @@ -23,7 +23,7 @@ export const postCaseReq: CasePostRequest = { connector: { id: 'none', name: 'none', - type: '.none' as ConnectorTypes, + type: ConnectorTypes.none, fields: null, }, settings: { diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 262e14fac6d8c..06d6dd7ac3b7a 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -15,7 +15,7 @@ import { export const getConfiguration = ({ id = 'connector-1', name = 'Connector 1', - type = '.none' as ConnectorTypes, + type = ConnectorTypes.none, fields = null, }: Partial = {}): CasesConfigureRequest => { return { @@ -32,6 +32,7 @@ export const getConfiguration = ({ export const getConfigurationOutput = (update = false): Partial => { return { ...getConfiguration(), + mappings: [], created_by: { email: null, full_name: null, username: 'elastic' }, updated_by: update ? { email: null, full_name: null, username: 'elastic' } : null, }; @@ -46,26 +47,6 @@ export const getServiceNowConnector = () => ({ }, config: { apiUrl: 'http://some.non.existent.com', - incidentConfiguration: { - mapping: [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'append', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, - isCaseOwned: true, }, }); @@ -79,96 +60,29 @@ export const getJiraConnector = () => ({ config: { apiUrl: 'http://some.non.existent.com', projectKey: 'pkey', - incidentConfiguration: { - mapping: [ - { - source: 'title', - target: 'summary', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, - isCaseOwned: true, }, }); -export const getResilientConnector = () => ({ - name: 'Resilient Connector', - actionTypeId: '.resilient', - secrets: { - apiKeyId: 'id', - apiKeySecret: 'secret', +export const getMappings = () => [ + { + source: 'title', + target: 'name', + actionType: 'overwrite', }, - config: { - apiUrl: 'http://some.non.existent.com', - orgId: 'pkey', - incidentConfiguration: { - mapping: [ - { - source: 'title', - target: 'name', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, - isCaseOwned: true, - }, -}); - -export const getConnectorWithoutCaseOwned = () => ({ - name: 'Connector without isCaseOwned', - actionTypeId: '.resilient', - secrets: { - apiKeyId: 'id', - apiKeySecret: 'secret', + { + source: 'description', + target: 'description', + actionType: 'overwrite', }, - config: { - apiUrl: 'http://some.non.existent.com', - orgId: 'pkey', - incidentConfiguration: { - mapping: [ - { - source: 'title', - target: 'name', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, + { + source: 'comments', + target: 'comments', + actionType: 'append', }, -}); +]; -export const getConnectorWithoutMapping = () => ({ - name: 'Connector without mapping', +export const getResilientConnector = () => ({ + name: 'Resilient Connector', actionTypeId: '.resilient', secrets: { apiKeyId: 'id', From 724b90e01b1df8fc15b30f9d3f8322740fd6aab9 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 15 Dec 2020 09:10:17 -0500 Subject: [PATCH 03/10] [Fleet] Fix agent details logs on safari (#85884) --- .../components/agent_logs/agent_logs.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx index 00deeff89503f..95c630e3b3686 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx @@ -16,6 +16,7 @@ import { EuiPanel, EuiButtonEmpty, } from '@elastic/eui'; +import useMeasure from 'react-use/lib/useMeasure'; import { FormattedMessage } from '@kbn/i18n/react'; import semverGte from 'semver/functions/gte'; import semverCoerce from 'semver/functions/coerce'; @@ -180,6 +181,8 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen [http.basePath, state.start, state.end, logStreamQuery] ); + const [logsPanelRef, { height: logPanelHeight }] = useMeasure(); + const agentVersion = agent.local_metadata?.elastic?.agent?.version; const isLogLevelSelectionAvailable = useMemo(() => { if (!agentVersion) { @@ -259,9 +262,9 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen - + Date: Tue, 15 Dec 2020 15:19:45 +0100 Subject: [PATCH 04/10] [APM] Increase Logstream height (#85913) Increasing the component height for more optimal viewing experience. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx index 331fa17ba8bf8..11d5987a74f76 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx @@ -135,6 +135,7 @@ function LogsTabContent({ transaction }: { transaction: Transaction }) { startTimestamp={startTimestamp - framePaddingMs} endTimestamp={endTimestamp + framePaddingMs} query={`trace.id:"${transaction.trace.id}" OR "${transaction.trace.id}"`} + height={640} /> ); } From 579ead8eafd710ecb78b71d6beabf2b7af4ca4cb Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 15 Dec 2020 09:37:21 -0500 Subject: [PATCH 05/10] Handle multiple monaco editor instances for Painless lang (#85834) --- .../src/painless/diagnostics_adapter.ts | 38 +++++++++++++------ .../src/painless/worker/painless_worker.ts | 16 +++++--- .../processor_form/processors/script.tsx | 2 +- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/packages/kbn-monaco/src/painless/diagnostics_adapter.ts b/packages/kbn-monaco/src/painless/diagnostics_adapter.ts index 95c4ec19cea1f..3535e1e3373e0 100644 --- a/packages/kbn-monaco/src/painless/diagnostics_adapter.ts +++ b/packages/kbn-monaco/src/painless/diagnostics_adapter.ts @@ -32,13 +32,28 @@ export class DiagnosticsAdapter { constructor(private worker: WorkerAccessor) { const onModelAdd = (model: monaco.editor.IModel): void => { let handle: any; - model.onDidChangeContent(() => { - // Every time a new change is made, wait 500ms before validating - clearTimeout(handle); - handle = setTimeout(() => this.validate(model.uri), 500); - }); - this.validate(model.uri); + if (model.getModeId() === ID) { + model.onDidChangeContent(() => { + // Do not validate if the language ID has changed + if (model.getModeId() !== ID) { + return; + } + + // Every time a new change is made, wait 500ms before validating + clearTimeout(handle); + handle = setTimeout(() => this.validate(model.uri), 500); + }); + + model.onDidChangeLanguage(({ newLanguage }) => { + // Reset the model markers if the language ID has changed and is no longer "painless" + if (newLanguage !== ID) { + return monaco.editor.setModelMarkers(model, ID, []); + } + }); + + this.validate(model.uri); + } }; monaco.editor.onDidCreateModel(onModelAdd); monaco.editor.getModels().forEach(onModelAdd); @@ -46,11 +61,12 @@ export class DiagnosticsAdapter { private async validate(resource: monaco.Uri): Promise { const worker = await this.worker(resource); - const errorMarkers = await worker.getSyntaxErrors(); - - const model = monaco.editor.getModel(resource); + const errorMarkers = await worker.getSyntaxErrors(resource.toString()); - // Set the error markers and underline them with "Error" severity - monaco.editor.setModelMarkers(model!, ID, errorMarkers.map(toDiagnostics)); + if (errorMarkers) { + const model = monaco.editor.getModel(resource); + // Set the error markers and underline them with "Error" severity + monaco.editor.setModelMarkers(model!, ID, errorMarkers.map(toDiagnostics)); + } } } diff --git a/packages/kbn-monaco/src/painless/worker/painless_worker.ts b/packages/kbn-monaco/src/painless/worker/painless_worker.ts index ce4ba024a4caa..35138dfdd4417 100644 --- a/packages/kbn-monaco/src/painless/worker/painless_worker.ts +++ b/packages/kbn-monaco/src/painless/worker/painless_worker.ts @@ -28,14 +28,18 @@ export class PainlessWorker { this._ctx = ctx; } - private getTextDocument(): string { - const model = this._ctx.getMirrorModels()[0]; - return model.getValue(); + private getTextDocument(modelUri: string): string | undefined { + const model = this._ctx.getMirrorModels().find((m) => m.uri.toString() === modelUri); + + return model?.getValue(); } - public async getSyntaxErrors() { - const code = this.getTextDocument(); - return parseAndGetSyntaxErrors(code); + public async getSyntaxErrors(modelUri: string) { + const code = this.getTextDocument(modelUri); + + if (code) { + return parseAndGetSyntaxErrors(code); + } } public provideAutocompleteSuggestions( diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/script.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/script.tsx index 8685738b39273..a62d9a79adb99 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/script.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/script.tsx @@ -129,7 +129,7 @@ const fieldsConfig: FieldsConfig = { export const Script: FormFieldsComponent = ({ initialFieldValues }) => { const [showId, setShowId] = useState(() => !!initialFieldValues?.id); - const [scriptLanguage, setScriptLanguage] = useState('plaintext'); + const [scriptLanguage, setScriptLanguage] = useState(PainlessLang.ID); const [{ fields }] = useFormData({ watch: 'fields.lang' }); From 853f30e23dd177790e8b57e8af242ec89998321a Mon Sep 17 00:00:00 2001 From: ymao1 Date: Tue, 15 Dec 2020 09:58:57 -0500 Subject: [PATCH 06/10] [Alerts] Remove Add Alerts flyout onClose (#85462) * Remove add alerts flyout after onClose * Updating tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Gidi Meir Morris --- .../public/components/create_alert.tsx | 13 ++++++++----- .../alerting/alerting_flyout/index.tsx | 14 +++++++++----- .../inventory/components/alert_flyout.tsx | 9 ++++----- .../log_threshold/components/alert_flyout.tsx | 13 ++++++------- .../components/alert_flyout.tsx | 12 ++++++------ .../sections/alert_form/alert_add.test.tsx | 3 +-- .../sections/alert_form/alert_add.tsx | 14 ++++---------- .../components/alerts_list.test.tsx | 12 +++++++++++- .../alerts_list/components/alerts_list.tsx | 19 +++++++++++-------- .../alerts/uptime_alerts_flyout_wrapper.tsx | 13 +++++++------ 10 files changed, 67 insertions(+), 55 deletions(-) diff --git a/x-pack/examples/alerting_example/public/components/create_alert.tsx b/x-pack/examples/alerting_example/public/components/create_alert.tsx index 58f9542709623..db7667411a27e 100644 --- a/x-pack/examples/alerting_example/public/components/create_alert.tsx +++ b/x-pack/examples/alerting_example/public/components/create_alert.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useCallback } from 'react'; import { EuiIcon, EuiFlexItem, EuiCard, EuiFlexGroup } from '@elastic/eui'; @@ -16,15 +16,18 @@ export const CreateAlert = ({ }: Pick) => { const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false); + const onCloseAlertFlyout = useCallback(() => setAlertFlyoutVisibility(false), [ + setAlertFlyoutVisibility, + ]); + const AddAlertFlyout = useMemo( () => triggersActionsUi.getAddAlertFlyout({ consumer: ALERTING_EXAMPLE_APP_ID, - addFlyoutVisible: alertFlyoutVisible, - setAddFlyoutVisibility: setAlertFlyoutVisibility, + onClose: onCloseAlertFlyout, }), // eslint-disable-next-line react-hooks/exhaustive-deps - [alertFlyoutVisible] + [onCloseAlertFlyout] ); return ( @@ -37,7 +40,7 @@ export const CreateAlert = ({ onClick={() => setAlertFlyoutVisibility(true)} /> - {AddAlertFlyout} + {alertFlyoutVisible && AddAlertFlyout} ); }; diff --git a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx index aa1d21dd1d580..88a897d7baf50 100644 --- a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { AlertType } from '../../../../common/alert_types'; import { TriggersAndActionsUIPublicPluginStart } from '../../../../../triggers_actions_ui/public'; @@ -23,17 +23,21 @@ export function AlertingFlyout(props: Props) { const { services: { triggersActionsUi }, } = useKibana(); + + const onCloseAddFlyout = useCallback(() => setAddFlyoutVisibility(false), [ + setAddFlyoutVisibility, + ]); + const addAlertFlyout = useMemo( () => alertType && triggersActionsUi.getAddAlertFlyout({ consumer: 'apm', - addFlyoutVisible, - setAddFlyoutVisibility, + onClose: onCloseAddFlyout, alertTypeId: alertType, canChangeTrigger: false, }), - [addFlyoutVisible, alertType, setAddFlyoutVisibility, triggersActionsUi] + [alertType, onCloseAddFlyout, triggersActionsUi] ); - return <>{addAlertFlyout}; + return <>{addFlyoutVisible && addAlertFlyout}; } diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx index 432d2073d93b6..1c6852c5eb8d9 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useMemo } from 'react'; +import React, { useCallback, useContext, useMemo } from 'react'; import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -26,14 +26,13 @@ export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }: const { inventoryPrefill } = useAlertPrefillContext(); const { customMetrics } = inventoryPrefill; - + const onCloseFlyout = useCallback(() => setVisible(false), [setVisible]); const AddAlertFlyout = useMemo( () => triggersActionsUI && triggersActionsUI.getAddAlertFlyout({ consumer: 'infrastructure', - addFlyoutVisible: visible!, - setAddFlyoutVisibility: setVisible, + onClose: onCloseFlyout, canChangeTrigger: false, alertTypeId: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, metadata: { @@ -47,5 +46,5 @@ export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }: [triggersActionsUI, visible] ); - return <>{AddAlertFlyout}; + return <>{visible && AddAlertFlyout}; }; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx index 206621c4d4dc8..fe8493ccd0fbf 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useMemo } from 'react'; +import React, { useCallback, useContext, useMemo } from 'react'; import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../../../../common/alerting/logs/log_threshold/types'; @@ -14,24 +14,23 @@ interface Props { } export const AlertFlyout = (props: Props) => { + const { visible, setVisible } = props; const { triggersActionsUI } = useContext(TriggerActionsContext); - + const onCloseFlyout = useCallback(() => setVisible(false), [setVisible]); const AddAlertFlyout = useMemo( () => triggersActionsUI && triggersActionsUI.getAddAlertFlyout({ consumer: 'logs', - addFlyoutVisible: props.visible!, - setAddFlyoutVisibility: props.setVisible, + onClose: onCloseFlyout, canChangeTrigger: false, alertTypeId: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, metadata: { isInternal: true, }, }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [triggersActionsUI, props.visible] + [triggersActionsUI, onCloseFlyout] ); - return <>{AddAlertFlyout}; + return <>{visible && AddAlertFlyout}; }; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx index 779478a313b71..72782f555d9ca 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useMemo } from 'react'; +import React, { useCallback, useContext, useMemo } from 'react'; import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types'; @@ -19,15 +19,15 @@ interface Props { } export const AlertFlyout = (props: Props) => { + const { visible, setVisible } = props; const { triggersActionsUI } = useContext(TriggerActionsContext); - + const onCloseFlyout = useCallback(() => setVisible(false), [setVisible]); const AddAlertFlyout = useMemo( () => triggersActionsUI && triggersActionsUI.getAddAlertFlyout({ consumer: 'infrastructure', - addFlyoutVisible: props.visible!, - setAddFlyoutVisibility: props.setVisible, + onClose: onCloseFlyout, canChangeTrigger: false, alertTypeId: METRIC_THRESHOLD_ALERT_TYPE_ID, metadata: { @@ -36,8 +36,8 @@ export const AlertFlyout = (props: Props) => { }, }), // eslint-disable-next-line react-hooks/exhaustive-deps - [triggersActionsUI, props.visible] + [triggersActionsUI, onCloseFlyout] ); - return <>{AddAlertFlyout}; + return <>{visible && AddAlertFlyout}; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index e068b1274b89a..2790ea8aa6bfa 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -129,8 +129,7 @@ describe('alert_add', () => { wrapper = mountWithIntl( {}} + onClose={() => {}} initialValues={initialValues} reloadAlerts={() => { return new Promise(() => {}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index 5ab2c7f5a586c..c432f68e71ef4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -26,10 +26,9 @@ import { useKibana } from '../../../common/lib/kibana'; export interface AlertAddProps> { consumer: string; - addFlyoutVisible: boolean; alertTypeRegistry: AlertTypeRegistryContract; actionTypeRegistry: ActionTypeRegistryContract; - setAddFlyoutVisibility: React.Dispatch>; + onClose: () => void; alertTypeId?: string; canChangeTrigger?: boolean; initialValues?: Partial; @@ -39,10 +38,9 @@ export interface AlertAddProps> { const AlertAdd = ({ consumer, - addFlyoutVisible, alertTypeRegistry, actionTypeRegistry, - setAddFlyoutVisibility, + onClose, canChangeTrigger, alertTypeId, initialValues, @@ -92,9 +90,9 @@ const AlertAdd = ({ }, [alertTypeId]); const closeFlyout = useCallback(() => { - setAddFlyoutVisibility(false); setAlert(initialAlert); - }, [initialAlert, setAddFlyoutVisibility]); + onClose(); + }, [initialAlert, onClose]); const saveAlertAndCloseFlyout = async () => { const savedAlert = await onSaveAlert(); @@ -107,10 +105,6 @@ const AlertAdd = ({ } }; - if (!addFlyoutVisible) { - return null; - } - const alertType = alert.alertTypeId ? alertTypeRegistry.get(alert.alertTypeId) : null; const errors = { ...(alertType ? alertType.validate(alert.params).errors : []), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index a576d811e9f8d..7df5c6e157106 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -26,6 +26,7 @@ jest.mock('../../../lib/action_connector_api', () => ({ jest.mock('../../../lib/alert_api', () => ({ loadAlerts: jest.fn(), loadAlertTypes: jest.fn(), + health: jest.fn(() => ({ isSufficientlySecure: true, hasPermanentEncryptionKey: true })), })); jest.mock('react-router-dom', () => ({ useHistory: () => ({ @@ -115,7 +116,16 @@ describe('alerts_list component empty', () => { expect( wrapper.find('[data-test-subj="createFirstAlertButton"]').find('EuiButton') ).toHaveLength(1); - expect(wrapper.find('AlertAdd')).toHaveLength(1); + expect(wrapper.find('AlertAdd').exists()).toBeFalsy(); + + wrapper.find('button[data-test-subj="createFirstAlertButton"]').simulate('click'); + + // When the AlertAdd component is rendered, it waits for the healthcheck to resolve + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + wrapper.update(); + expect(wrapper.find('AlertAdd').exists()).toEqual(true); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index bf0d97d418d5c..1369e6e8f3b82 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -676,14 +676,17 @@ export const AlertsList: React.FunctionComponent = () => { ) : ( noPermissionPrompt )} - + {alertFlyoutVisible && ( + { + setAlertFlyoutVisibility(false); + }} + actionTypeRegistry={actionTypeRegistry} + alertTypeRegistry={alertTypeRegistry} + reloadAlerts={loadAlertsData} + /> + )} ); }; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx index 7995cf88df9ba..75cbd43cd0b38 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { TriggersAndActionsUIPublicPluginStart } from '../../../../../../plugins/triggers_actions_ui/public'; @@ -24,19 +24,20 @@ export const UptimeAlertsFlyoutWrapperComponent = ({ setAlertFlyoutVisibility, }: Props) => { const { triggersActionsUi } = useKibana().services; - + const onCloseAlertFlyout = useCallback(() => setAlertFlyoutVisibility(false), [ + setAlertFlyoutVisibility, + ]); const AddAlertFlyout = useMemo( () => triggersActionsUi.getAddAlertFlyout({ consumer: 'uptime', - addFlyoutVisible: alertFlyoutVisible, - setAddFlyoutVisibility: setAlertFlyoutVisibility, + onClose: onCloseAlertFlyout, alertTypeId, canChangeTrigger: !alertTypeId, }), // eslint-disable-next-line react-hooks/exhaustive-deps - [alertFlyoutVisible, alertTypeId] + [onCloseAlertFlyout, alertTypeId] ); - return <>{AddAlertFlyout}; + return <>{alertFlyoutVisible && AddAlertFlyout}; }; From 5fdace379c52f1feba77b6c9ccb0cd92bb8da9cb Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 15 Dec 2020 15:02:26 +0000 Subject: [PATCH 07/10] skip flaky suite (#85714) --- .../public/resolver/view/panels/node_events_of_type.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx index 4d3ccaf278c91..c462bd1e3553e 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx @@ -15,7 +15,7 @@ import { urlSearch } from '../../test_utilities/url_search'; const resolverComponentInstanceID = 'resolverComponentInstanceID'; // FLAKY: https://github.com/elastic/kibana/issues/85714 -describe(`Resolver: when analyzing a tree with only the origin and paginated related events, and when the component instance ID is ${resolverComponentInstanceID}`, () => { +describe.skip(`Resolver: when analyzing a tree with only the origin and paginated related events, and when the component instance ID is ${resolverComponentInstanceID}`, () => { /** * Get (or lazily create and get) the simulator. */ From 81744dcf6b20e1ed29dcd39493e0fab40a67fcac Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 15 Dec 2020 15:05:39 +0000 Subject: [PATCH 08/10] skip flaky suite (#85911) --- test/api_integration/apis/saved_objects/find.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/api_integration/apis/saved_objects/find.js b/test/api_integration/apis/saved_objects/find.js index eb75358c7c77c..60f4db3376ea5 100644 --- a/test/api_integration/apis/saved_objects/find.js +++ b/test/api_integration/apis/saved_objects/find.js @@ -78,7 +78,8 @@ export default function ({ getService }) { })); }); - describe('page beyond total', () => { + // FLAKY: https://github.com/elastic/kibana/issues/85911 + describe.skip('page beyond total', () => { it('should return 200 with empty response', async () => await supertest .get('/api/saved_objects/_find?type=visualization&page=100&per_page=100') From 47ffefec09114148ceb4c9e8af18d56b18586c11 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 15 Dec 2020 15:15:11 +0000 Subject: [PATCH 09/10] skip flaky suite (#85899) --- .../monitor/synthetics/__tests__/executed_step.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_step.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_step.test.tsx index 6864dc0eb7cf5..e3a3d39241de2 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_step.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_step.test.tsx @@ -9,7 +9,8 @@ import { ExecutedStep } from '../executed_step'; import { Ping } from '../../../../../common/runtime_types'; import { mountWithRouter } from '../../../../lib'; -describe('ExecutedStep', () => { +// FLAKY: https://github.com/elastic/kibana/issues/85899 +describe.skip('ExecutedStep', () => { let step: Ping; beforeEach(() => { From 04d4e71ff5d563c1758292cabaf220d6d23b3d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 15 Dec 2020 16:34:11 +0100 Subject: [PATCH 10/10] removing margin for sparkline charts (#85917) --- .../apm/public/components/shared/charts/spark_plot/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx index 0917839ad631b..e620acd56aadd 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx @@ -46,6 +46,7 @@ export function SparkPlot({ const defaultChartTheme = useChartTheme(); const sparkplotChartTheme = merge({}, defaultChartTheme, { + chartMargins: { left: 0, right: 0, top: 0, bottom: 0 }, lineSeriesStyle: { point: { opacity: 0 }, },