From 85ef5c037d3dc08e98a8609ef7c6ab2175731aec Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 6 May 2020 11:26:25 +0300 Subject: [PATCH] [SIEM][CASE] Configuration pages UI redesign (#65355) (#65414) --- .../configure_cases/connectors.test.tsx | 16 ++ .../components/configure_cases/connectors.tsx | 61 +++-- .../components/configure_cases/index.test.tsx | 218 +++++++++++------- .../case/components/configure_cases/index.tsx | 58 +---- .../configure_cases/translations.ts | 11 +- 5 files changed, 219 insertions(+), 145 deletions(-) diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx index 125a42b126466..b0271f6849ac5 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx @@ -16,13 +16,17 @@ describe('Connectors', () => { let wrapper: ReactWrapper; const onChangeConnector = jest.fn(); const handleShowAddFlyout = jest.fn(); + const handleShowEditFlyout = jest.fn(); + const props: Props = { disabled: false, + updateConnectorDisabled: false, connectors, selectedConnector: 'none', isLoading: false, onChangeConnector, handleShowAddFlyout, + handleShowEditFlyout, }; beforeAll(() => { @@ -87,4 +91,16 @@ describe('Connectors', () => { expect(onChangeConnector).toHaveBeenCalled(); expect(onChangeConnector).toHaveBeenCalledWith('none'); }); + + test('the text of the update button is shown correctly', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect( + newWrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .text() + ).toBe('Update My Connector'); + }); }); diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx index de6d5f76cfad0..1b1439d3bac43 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiDescribedFormGroup, EuiFormRow, EuiFlexGroup, EuiFlexItem, EuiLink, + EuiButton, } from '@elastic/eui'; import styled from 'styled-components'; @@ -28,35 +29,55 @@ const EuiFormRowExtended = styled(EuiFormRow)` } `; +const AddConnectorEuiFormRow = styled(EuiFormRow)` + width: 100%; + max-width: 100%; + text-align: right; +`; + export interface Props { connectors: Connector[]; disabled: boolean; isLoading: boolean; + updateConnectorDisabled: boolean; onChangeConnector: (id: string) => void; selectedConnector: string; handleShowAddFlyout: () => void; + handleShowEditFlyout: () => void; } const ConnectorsComponent: React.FC = ({ connectors, - disabled, isLoading, + disabled, + updateConnectorDisabled, onChangeConnector, selectedConnector, handleShowAddFlyout, + handleShowEditFlyout, }) => { - const dropDownLabel = ( - - {i18n.INCIDENT_MANAGEMENT_SYSTEM_LABEL} - - - {i18n.ADD_NEW_CONNECTOR} - - - + const connectorsName = useMemo( + () => connectors.find(c => c.id === selectedConnector)?.name ?? 'none', + [connectors, selectedConnector] + ); + + const dropDownLabel = useMemo( + () => ( + + {i18n.INCIDENT_MANAGEMENT_SYSTEM_LABEL} + + {connectorsName !== 'none' && ( + + {i18n.UPDATE_SELECTED_CONNECTOR(connectorsName)} + + )} + + + ), + [connectorsName] ); return ( @@ -81,6 +102,16 @@ const ConnectorsComponent: React.FC = ({ data-test-subj="case-connectors-dropdown" /> + + + {i18n.ADD_NEW_CONNECTOR} + + ); diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx index 0359c1dbdba67..08975703241c7 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx @@ -11,13 +11,11 @@ import { ConfigureCases } from './'; import { TestProviders } from '../../../../mock'; import { Connectors } from './connectors'; import { ClosureOptions } from './closure_options'; -import { Mapping } from './mapping'; import { ActionsConnectorsContextProvider, ConnectorAddFlyout, ConnectorEditFlyout, } from '../../../../../../triggers_actions_ui/public'; -import { EuiBottomBar } from '@elastic/eui'; import { useKibana } from '../../../../lib/kibana'; import { useConnectors } from '../../../../containers/case/configure/use_connectors'; @@ -55,17 +53,11 @@ describe('ConfigureCases', () => { }); test('it renders the Connectors', () => { - expect(wrapper.find('[data-test-subj="case-connectors-form-group"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="dropdown-connectors"]').exists()).toBeTruthy(); }); test('it renders the ClosureType', () => { - expect( - wrapper.find('[data-test-subj="case-closure-options-form-group"]').exists() - ).toBeTruthy(); - }); - - test('it renders the Mapping', () => { - expect(wrapper.find('[data-test-subj="case-mapping-form-group"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="closure-options-radio-group"]').exists()).toBeTruthy(); }); test('it renders the ActionsConnectorsContextProvider', () => { @@ -127,8 +119,12 @@ describe('ConfigureCases', () => { ).toBeTruthy(); }); - test('it disables the update connector button when the connectorId is invalid', () => { - expect(wrapper.find(Mapping).prop('disabled')).toBe(true); + test('it hides the update connector button when the connectorId is invalid', () => { + expect( + wrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .exists() + ).toBeFalsy(); }); }); @@ -171,14 +167,6 @@ describe('ConfigureCases', () => { expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(false); expect(wrapper.find(ClosureOptions).prop('closureTypeSelected')).toBe('close-by-user'); - // Mapping - expect(wrapper.find(Mapping).prop('disabled')).toBe(true); - expect(wrapper.find(Mapping).prop('updateConnectorDisabled')).toBe(false); - expect(wrapper.find(Mapping).prop('connectorActionTypeId')).toBe('.servicenow'); - expect(wrapper.find(Mapping).prop('mapping')).toEqual( - connectors[0].config.casesConfiguration.mapping - ); - // Flyouts expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(false); expect(wrapper.find(ConnectorAddFlyout).prop('actionTypes')).toEqual([ @@ -200,28 +188,41 @@ describe('ConfigureCases', () => { ).toBeFalsy(); }); - test('it disables the mapping permanently', () => { - expect(wrapper.find(Mapping).prop('disabled')).toBe(true); - }); - - test('it sets the mapping of a connector correctly', () => { - expect(wrapper.find(Mapping).prop('mapping')).toEqual( - connectors[0].config.casesConfiguration.mapping - ); - }); - - // TODO: When mapping is enabled the test.todo should be implemented. - test.todo('it disables the update connector button when loading the configuration'); - test('it disables correctly when the user cannot crud', () => { const newWrapper = mount(, { wrappingComponent: TestProviders, }); - expect(newWrapper.find(Connectors).prop('disabled')).toBe(true); - expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true); - expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); - expect(newWrapper.find(Mapping).prop('updateConnectorDisabled')).toBe(true); + expect(newWrapper.find('button[data-test-subj="dropdown-connectors"]').prop('disabled')).toBe( + true + ); + + expect( + newWrapper + .find('button[data-test-subj="case-configure-add-connector-button"]') + .prop('disabled') + ).toBe(true); + + expect( + newWrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .prop('disabled') + ).toBe(true); + + // Two closure options + expect( + newWrapper + .find('[data-test-subj="closure-options-radio-group"] input') + .first() + .prop('disabled') + ).toBe(true); + + expect( + newWrapper + .find('[data-test-subj="closure-options-radio-group"] input') + .at(1) + .prop('disabled') + ).toBe(true); }); }); @@ -232,7 +233,18 @@ describe('ConfigureCases', () => { jest.resetAllMocks(); jest.restoreAllMocks(); jest.clearAllMocks(); - useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[1].config.casesConfiguration.mapping, + closureType: 'close-by-user', + connectorId: 'servicenow-2', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: 'servicenow-1', + closureType: 'close-by-user', + }, + })); useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, loading: true, @@ -243,7 +255,9 @@ describe('ConfigureCases', () => { }); test('it disables correctly Connector when loading connectors', () => { - expect(wrapper.find(Connectors).prop('disabled')).toBe(true); + expect( + wrapper.find('button[data-test-subj="dropdown-connectors"]').prop('disabled') + ).toBeTruthy(); }); test('it pass the correct value to isLoading attribute on Connector', () => { @@ -254,38 +268,31 @@ describe('ConfigureCases', () => { expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(true); }); - test('it disables the update connector button when loading the connectors', () => { - expect(wrapper.find(Mapping).prop('disabled')).toBe(true); + test('it hides the update connector button when loading the connectors', () => { + expect( + wrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .prop('disabled') + ).toBe(true); }); + test('it disables the buttons of action bar when loading connectors', () => { - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-2', - connectorName: 'unchanged', - currentConfiguration: { - connectorName: 'unchanged', - connectorId: 'servicenow-1', - closureType: 'close-by-user', - }, - })); const newWrapper = mount(, { wrappingComponent: TestProviders, }); expect( newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .find('button[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') .first() - .prop('isDisabled') + .prop('disabled') ).toBe(true); expect( newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .find('button[data-test-subj="case-configure-action-bottom-bar-save-button"]') .first() - .prop('isDisabled') + .prop('disabled') ).toBe(true); }); }); @@ -297,6 +304,7 @@ describe('ConfigureCases', () => { jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, + connectorId: 'servicenow-1', persistLoading: true, })); @@ -311,11 +319,27 @@ describe('ConfigureCases', () => { }); test('it disables correctly ClosureOptions when saving configuration', () => { - expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(true); + expect( + wrapper + .find('[data-test-subj="closure-options-radio-group"] input') + .first() + .prop('disabled') + ).toBe(true); + + expect( + wrapper + .find('[data-test-subj="closure-options-radio-group"] input') + .at(1) + .prop('disabled') + ).toBe(true); }); test('it disables the update connector button when saving the configuration', () => { - expect(wrapper.find(Mapping).prop('disabled')).toBe(true); + expect( + wrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .prop('disabled') + ).toBe(true); }); test('it disables the buttons of action bar when saving configuration', () => { @@ -387,6 +411,32 @@ describe('ConfigureCases', () => { }); }); + describe('loading configuration', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + loading: true, + })); + useConnectorsMock.mockImplementation(() => ({ + ...useConnectorsResponse, + })); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it hides the update connector button when loading the configuration', () => { + expect( + wrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .exists() + ).toBeFalsy(); + }); + }); + describe('update connector', () => { let wrapper: ReactWrapper; const persistCaseConfigure = jest.fn(); @@ -500,18 +550,22 @@ describe('ConfigureCases', () => { wrapper.update(); expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true); - expect(wrapper.find(EuiBottomBar).exists()).toBeFalsy(); + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); }); test('it show the edit flyout when pressing the update connector button', () => { const wrapper = mount(, { wrappingComponent: TestProviders }); wrapper - .find('button[data-test-subj="case-mapping-update-connector-button"]') + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') .simulate('click'); wrapper.update(); expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(true); - expect(wrapper.find(EuiBottomBar).exists()).toBeFalsy(); + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); }); test('it tracks the changes successfully', () => { @@ -681,7 +735,7 @@ describe('ConfigureCases', () => { // Press update connector button wrapper - .find('button[data-test-subj="case-mapping-update-connector-button"]') + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') .simulate('click'); wrapper.update(); @@ -778,29 +832,29 @@ describe('ConfigureCases', () => { ).toBeFalsy(); }); - test('it sets the mapping correctly when changing connector types', () => { - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[2].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'jira-1', - connectorName: 'unchanged', - currentConfiguration: { - connectorName: 'unchanged', + test('the text of the update button is changed successfully', () => { + useCaseConfigureMock + .mockImplementationOnce(() => ({ + ...useCaseConfigureResponse, connectorId: 'servicenow-1', - closureType: 'close-by-user', - }, - persistLoading: false, - })); + })) + .mockImplementation(() => ({ + ...useCaseConfigureResponse, + connectorId: 'servicenow-2', + })); const wrapper = mount(, { wrappingComponent: TestProviders }); + + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); + wrapper.update(); + expect( - wrapper.find('button[data-test-subj="case-configure-third-party-select-title"]').text() - ).toBe('Summary'); + wrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .text() + ).toBe('Update My Connector 2'); }); - - // TODO: When mapping is enabled the test.todo should be implemented. - test.todo('the mapping is changed successfully when changing the third party'); - test.todo('the mapping is changed successfully when changing the action type'); }); }); diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx index 40def5231a304..739083a5009ec 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useState, Dispatch, SetStateAction, useMemo } from 'react'; +import React, { useCallback, useEffect, useState, Dispatch, SetStateAction } from 'react'; import styled, { css } from 'styled-components'; import { @@ -16,7 +16,8 @@ import { EuiButtonEmpty, EuiText, } from '@elastic/eui'; -import { isEmpty, difference } from 'lodash/fp'; + +import { difference } from 'lodash/fp'; import { useKibana } from '../../../../lib/kibana'; import { useConnectors } from '../../../../containers/case/configure/use_connectors'; import { useCaseConfigure } from '../../../../containers/case/configure/use_configure'; @@ -31,12 +32,10 @@ import { import { ActionConnectorTableItem } from '../../../../../../triggers_actions_ui/public/types'; import { getCaseUrl } from '../../../../components/link_to'; import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; -import { CCMapsCombinedActionAttributes } from '../../../../containers/case/configure/types'; import { connectorsConfiguration } from '../../../../lib/connectors/config'; import { Connectors } from '../configure_cases/connectors'; import { ClosureOptions } from '../configure_cases/closure_options'; -import { Mapping } from '../configure_cases/mapping'; import { SectionWrapper } from '../wrappers'; import { navTabs } from '../../../../pages/home/home_navigations'; import * as i18n from './translations'; @@ -79,14 +78,12 @@ const ConfigureCasesComponent: React.FC = ({ userC const { connectorId, closureType, - mapping, currentConfiguration, loading: loadingCaseConfigure, persistLoading, persistCaseConfigure, setConnector, setClosureType, - setMapping, } = useCaseConfigure(); const { loading: isLoadingConnectors, connectors, refetchConnectors } = useConnectors(); @@ -107,7 +104,7 @@ const ConfigureCasesComponent: React.FC = ({ userC closureType, }); }, - [connectorId, connectors, closureType, mapping] + [connectorId, connectors, closureType] ); const onClickAddConnector = useCallback(() => { @@ -149,24 +146,6 @@ const ConfigureCasesComponent: React.FC = ({ userC [currentConfiguration, connectorId, closureType] ); - useEffect(() => { - if ( - !isEmpty(connectors) && - connectorId !== 'none' && - connectors.some(c => c.id === connectorId) - ) { - const myConnector = connectors.find(c => c.id === connectorId); - const myMapping = myConnector?.config?.casesConfiguration?.mapping ?? []; - setMapping( - myMapping.map((m: CCMapsCombinedActionAttributes) => ({ - source: m.source, - target: m.target, - actionType: m.action_type ?? m.actionType, - })) - ); - } - }, [connectors, connectorId]); - useEffect(() => { if ( !isLoadingConnectors && @@ -200,11 +179,6 @@ const ConfigureCasesComponent: React.FC = ({ userC currentConfiguration.closureType, ]); - const connectorActionTypeId = useMemo( - () => connectors.find(c => c.id === connectorId)?.actionTypeId ?? '.none', - [connectorId, connectors] - ); - return ( {!connectorIsValid && ( @@ -219,16 +193,6 @@ const ConfigureCasesComponent: React.FC = ({ userC )} - - - = ({ userC /> - {actionBarVisible && ( diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/translations.ts b/x-pack/plugins/siem/public/pages/case/components/configure_cases/translations.ts index 46d713773a837..58438d5cd666a 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/translations.ts +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/translations.ts @@ -33,13 +33,13 @@ export const NO_CONNECTOR = i18n.translate('xpack.siem.case.configureCases.noCon }); export const ADD_NEW_CONNECTOR = i18n.translate('xpack.siem.case.configureCases.addNewConnector', { - defaultMessage: 'Add new connector option', + defaultMessage: 'Add new connector', }); export const CASE_CLOSURE_OPTIONS_TITLE = i18n.translate( 'xpack.siem.case.configureCases.caseClosureOptionsTitle', { - defaultMessage: 'Cases Closures', + defaultMessage: 'Case Closures', } ); @@ -170,6 +170,13 @@ export const UPDATE_CONNECTOR = i18n.translate('xpack.siem.case.configureCases.u defaultMessage: 'Update connector', }); +export const UPDATE_SELECTED_CONNECTOR = (connectorName: string): string => { + return i18n.translate('xpack.siem.case.configureCases.updateSelectedConnector', { + values: { connectorName }, + defaultMessage: 'Update { connectorName }', + }); +}; + export const UNSAVED_CHANGES = (unsavedChanges: number): string => { return i18n.translate('xpack.siem.case.configureCases.unsavedChanges', { values: { unsavedChanges },