diff --git a/x-pack/plugins/siem/cypress/integration/cases_connectors.spec.ts b/x-pack/plugins/siem/cypress/integration/cases_connectors.spec.ts index 2d650b1bbd9d1..6beb936d15ee6 100644 --- a/x-pack/plugins/siem/cypress/integration/cases_connectors.spec.ts +++ b/x-pack/plugins/siem/cypress/integration/cases_connectors.spec.ts @@ -11,7 +11,6 @@ import { goToEditExternalConnection } from '../tasks/all_cases'; import { addServiceNowConnector, openAddNewConnectorOption, - saveChanges, selectLastConnectorCreated, } from '../tasks/configure_cases'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; @@ -37,7 +36,6 @@ describe('Cases connectors', () => { cy.get(TOASTER).should('have.text', "Created 'New connector'"); selectLastConnectorCreated(); - saveChanges(); cy.wait('@saveConnector', { timeout: 10000 }) .its('status') diff --git a/x-pack/plugins/siem/cypress/screens/configure_cases.ts b/x-pack/plugins/siem/cypress/screens/configure_cases.ts index 5a1e897c43e27..006c524a38acb 100644 --- a/x-pack/plugins/siem/cypress/screens/configure_cases.ts +++ b/x-pack/plugins/siem/cypress/screens/configure_cases.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export const ADD_NEW_CONNECTOR_OPTION_LINK = - '[data-test-subj="case-configure-add-connector-button"]'; +export const ADD_NEW_CONNECTOR_DROPDOWN_BUTTON = + '[data-test-subj="dropdown-connector-add-connector"]'; export const CONNECTOR = (id: string) => { return `[data-test-subj='dropdown-connector-${id}']`; diff --git a/x-pack/plugins/siem/cypress/tasks/configure_cases.ts b/x-pack/plugins/siem/cypress/tasks/configure_cases.ts index 9172e02708ae7..6ba9e875c7cb0 100644 --- a/x-pack/plugins/siem/cypress/tasks/configure_cases.ts +++ b/x-pack/plugins/siem/cypress/tasks/configure_cases.ts @@ -5,13 +5,12 @@ */ import { - ADD_NEW_CONNECTOR_OPTION_LINK, + ADD_NEW_CONNECTOR_DROPDOWN_BUTTON, CONNECTOR, CONNECTOR_NAME, CONNECTORS_DROPDOWN, PASSWORD, SAVE_BTN, - SAVE_CHANGES_BTN, SERVICE_NOW_CONNECTOR_CARD, URL, USERNAME, @@ -33,15 +32,12 @@ export const openAddNewConnectorOption = () => { cy.get(MAIN_PAGE).then($page => { if ($page.find(SERVICE_NOW_CONNECTOR_CARD).length !== 1) { cy.wait(1000); - cy.get(ADD_NEW_CONNECTOR_OPTION_LINK).click({ force: true }); + cy.get(CONNECTORS_DROPDOWN).click({ force: true }); + cy.get(ADD_NEW_CONNECTOR_DROPDOWN_BUTTON).click(); } }); }; -export const saveChanges = () => { - cy.get(SAVE_CHANGES_BTN).click(); -}; - export const selectLastConnectorCreated = () => { cy.get(CONNECTORS_DROPDOWN).click({ force: true }); cy.get('@createConnector') diff --git a/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx b/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx index 0ab90d8a73126..f157f654d5617 100644 --- a/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx @@ -19,53 +19,79 @@ describe('CaseCallOut ', () => { ...defaultProps, message: 'we have one message', }; + const wrapper = mount(); + expect( wrapper - .find(`[data-test-subj="callout-message"]`) + .find(`[data-test-subj="callout-message-primary"]`) .last() .exists() ).toBeTruthy(); + }); + + it('Renders multi message callout', () => { + const props = { + ...defaultProps, + messages: [ + { ...defaultProps, description:

{'we have two messages'}

}, + { ...defaultProps, description:

{'for real'}

}, + ], + }; + const wrapper = mount(); expect( wrapper - .find(`[data-test-subj="callout-messages"]`) + .find(`[data-test-subj="callout-message-primary"]`) .last() .exists() ).toBeFalsy(); + expect( + wrapper + .find(`[data-test-subj="callout-messages-primary"]`) + .last() + .exists() + ).toBeTruthy(); }); - it('Renders multi message callout', () => { + + it('it shows the correct type of callouts', () => { const props = { ...defaultProps, messages: [ - { ...defaultProps, description:

{'we have two messages'}

}, + { + ...defaultProps, + description:

{'we have two messages'}

, + errorType: 'danger' as 'primary' | 'success' | 'warning' | 'danger', + }, { ...defaultProps, description:

{'for real'}

}, ], }; const wrapper = mount(); expect( wrapper - .find(`[data-test-subj="callout-message"]`) + .find(`[data-test-subj="callout-messages-danger"]`) .last() .exists() - ).toBeFalsy(); + ).toBeTruthy(); + expect( wrapper - .find(`[data-test-subj="callout-messages"]`) + .find(`[data-test-subj="callout-messages-primary"]`) .last() .exists() ).toBeTruthy(); }); + it('Dismisses callout', () => { const props = { ...defaultProps, message: 'we have one message', }; const wrapper = mount(); - expect(wrapper.find(`[data-test-subj="case-call-out"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="case-call-out-primary"]`).exists()).toBeTruthy(); wrapper - .find(`[data-test-subj="callout-dismiss"]`) + .find(`[data-test-subj="callout-dismiss-primary"]`) .last() .simulate('click'); - expect(wrapper.find(`[data-test-subj="case-call-out"]`).exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="case-call-out-primary"]`).exists()).toBeFalsy(); }); }); diff --git a/x-pack/plugins/siem/public/cases/components/callout/index.tsx b/x-pack/plugins/siem/public/cases/components/callout/index.tsx index 0fc93af7f318d..5266c9aec9705 100644 --- a/x-pack/plugins/siem/public/cases/components/callout/index.tsx +++ b/x-pack/plugins/siem/public/cases/components/callout/index.tsx @@ -12,28 +12,69 @@ import * as i18n from './translations'; export * from './helpers'; +interface ErrorMessage { + title: string; + description: JSX.Element; + errorType?: 'primary' | 'success' | 'warning' | 'danger'; +} + interface CaseCallOutProps { title: string; message?: string; - messages?: Array<{ title: string; description: JSX.Element }>; + messages?: ErrorMessage[]; } const CaseCallOutComponent = ({ title, message, messages }: CaseCallOutProps) => { const [showCallOut, setShowCallOut] = useState(true); const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]); + let callOutMessages = messages ?? []; + + if (message) { + callOutMessages = [ + ...callOutMessages, + { + title: '', + description:

{message}

, + errorType: 'primary', + }, + ]; + } + + const groupedErrorMessages = callOutMessages.reduce((acc, currentMessage: ErrorMessage) => { + const key = currentMessage.errorType == null ? 'primary' : currentMessage.errorType; + return { + ...acc, + [key]: [...(acc[key] || []), currentMessage], + }; + }, {} as { [key in NonNullable]: ErrorMessage[] }); return showCallOut ? ( <> - - {!isEmpty(messages) && ( - - )} - {!isEmpty(message) &&

{message}

} - - {i18n.DISMISS_CALLOUT} - -
- + {(Object.keys(groupedErrorMessages) as Array).map(key => ( + + + {!isEmpty(groupedErrorMessages[key]) && ( + + )} + + {i18n.DISMISS_CALLOUT} + + + + + ))} ) : null; }; diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/connectors.test.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/connectors.test.tsx index 41cd3e549415d..4f6fad8491206 100644 --- a/x-pack/plugins/siem/public/cases/components/configure_cases/connectors.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/connectors.test.tsx @@ -15,7 +15,6 @@ import { connectors } from './__mock__'; describe('Connectors', () => { let wrapper: ReactWrapper; const onChangeConnector = jest.fn(); - const handleShowAddFlyout = jest.fn(); const handleShowEditFlyout = jest.fn(); const props: Props = { @@ -25,7 +24,6 @@ describe('Connectors', () => { selectedConnector: 'none', isLoading: false, onChangeConnector, - handleShowAddFlyout, handleShowEditFlyout, }; @@ -92,6 +90,15 @@ describe('Connectors', () => { expect(onChangeConnector).toHaveBeenCalledWith('none'); }); + test('it shows the add connector button', () => { + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + + expect( + wrapper.find('button[data-test-subj="dropdown-connector-add-connector"]').exists() + ).toBeTruthy(); + }); + test('the text of the update button is shown correctly', () => { const newWrapper = mount(, { wrappingComponent: TestProviders, diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/connectors.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/connectors.tsx index 3916ce297a0a4..76e816a2909ab 100644 --- a/x-pack/plugins/siem/public/cases/components/configure_cases/connectors.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/connectors.tsx @@ -11,7 +11,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, - EuiButton, } from '@elastic/eui'; import styled from 'styled-components'; @@ -29,12 +28,6 @@ const EuiFormRowExtended = styled(EuiFormRow)` } `; -const AddConnectorEuiFormRow = styled(EuiFormRow)` - width: 100%; - max-width: 100%; - text-align: right; -`; - export interface Props { connectors: Connector[]; disabled: boolean; @@ -42,7 +35,6 @@ export interface Props { updateConnectorDisabled: boolean; onChangeConnector: (id: string) => void; selectedConnector: string; - handleShowAddFlyout: () => void; handleShowEditFlyout: () => void; } const ConnectorsComponent: React.FC = ({ @@ -52,7 +44,6 @@ const ConnectorsComponent: React.FC = ({ updateConnectorDisabled, onChangeConnector, selectedConnector, - handleShowAddFlyout, handleShowEditFlyout, }) => { const connectorsName = useMemo( @@ -77,7 +68,7 @@ const ConnectorsComponent: React.FC = ({ ), - [connectorsName] + [connectorsName, updateConnectorDisabled] ); return ( @@ -100,18 +91,9 @@ const ConnectorsComponent: React.FC = ({ isLoading={isLoading} onChange={onChangeConnector} data-test-subj="case-connectors-dropdown" + appendAddConnectorButton={true} /> - - - {i18n.ADD_NEW_CONNECTOR} - - ); diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/connectors_dropdown.tsx index b2b2edb04bd29..c5481f592e750 100644 --- a/x-pack/plugins/siem/public/cases/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/connectors_dropdown.tsx @@ -18,6 +18,7 @@ export interface Props { isLoading: boolean; onChange: (id: string) => void; selectedConnector: string; + appendAddConnectorButton?: boolean; } const ICON_SIZE = 'm'; @@ -42,40 +43,55 @@ const noConnectorOption = { 'data-test-subj': 'dropdown-connector-no-connector', }; +const addNewConnector = { + value: 'add-connector', + inputDisplay: ( + + {i18n.ADD_NEW_CONNECTOR} + + ), + 'data-test-subj': 'dropdown-connector-add-connector', +}; + const ConnectorsDropdownComponent: React.FC = ({ connectors, disabled, isLoading, onChange, selectedConnector, + appendAddConnectorButton = false, }) => { - const connectorsAsOptions = useMemo( - () => - connectors.reduce( - (acc, connector) => [ - ...acc, - { - value: connector.id, - inputDisplay: ( - - - - - - {connector.name} - - - ), - 'data-test-subj': `dropdown-connector-${connector.id}`, - }, - ], - [noConnectorOption] - ), - [connectors] - ); + const connectorsAsOptions = useMemo(() => { + const connectorsFormatted = connectors.reduce( + (acc, connector) => [ + ...acc, + { + value: connector.id, + inputDisplay: ( + + + + + + {connector.name} + + + ), + 'data-test-subj': `dropdown-connector-${connector.id}`, + }, + ], + [noConnectorOption] + ); + + if (appendAddConnectorButton) { + return [...connectorsFormatted, addNewConnector]; + } + + return connectorsFormatted; + }, [connectors]); return ( { wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists() ).toBeFalsy(); }); - - test('it does NOT render the EuiBottomBar', () => { - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - }); - - test('it disables correctly ClosureOptions when the connector is set to none', () => { - expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(true); - }); }); describe('Unhappy path', () => { @@ -182,12 +172,6 @@ describe('ConfigureCases', () => { expect(wrapper.find(ConnectorEditFlyout).prop('initialConnector')).toEqual(connectors[0]); }); - test('it does not shows the action bar when there is no change', () => { - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - }); - test('it disables correctly when the user cannot crud', () => { const newWrapper = mount(, { wrappingComponent: TestProviders, @@ -197,12 +181,6 @@ describe('ConfigureCases', () => { 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"]') @@ -275,26 +253,6 @@ describe('ConfigureCases', () => { .prop('disabled') ).toBe(true); }); - - test('it disables the buttons of action bar when loading connectors', () => { - const newWrapper = mount(, { - wrappingComponent: TestProviders, - }); - - expect( - newWrapper - .find('button[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('disabled') - ).toBe(true); - - expect( - newWrapper - .find('button[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .prop('disabled') - ).toBe(true); - }); }); describe('saving configuration', () => { @@ -341,74 +299,6 @@ describe('ConfigureCases', () => { .prop('disabled') ).toBe(true); }); - - test('it disables the buttons of action bar when saving configuration', () => { - 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', - }, - persistLoading: true, - })); - - const newWrapper = mount(, { - wrappingComponent: TestProviders, - }); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - }); - - test('it shows the loading spinner when saving configuration', () => { - 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', - }, - persistLoading: true, - })); - - const newWrapper = mount(, { - wrappingComponent: TestProviders, - }); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('isLoading') - ).toBe(true); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .prop('isLoading') - ).toBe(true); - }); }); describe('loading configuration', () => { @@ -437,7 +327,7 @@ describe('ConfigureCases', () => { }); }); - describe('update connector', () => { + describe('connectors', () => { let wrapper: ReactWrapper; const persistCaseConfigure = jest.fn(); @@ -445,13 +335,13 @@ describe('ConfigureCases', () => { jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, + mapping: connectors[0].config.casesConfiguration.mapping, closureType: 'close-by-user', - connectorId: 'servicenow-2', - connectorName: 'unchanged', + connectorId: 'servicenow-1', + connectorName: 'My connector', currentConfiguration: { - connectorName: 'unchanged', - connectorId: 'servicenow-1', + connectorName: 'My connector', + connectorId: 'My connector', closureType: 'close-by-user', }, persistCaseConfigure, @@ -463,12 +353,10 @@ describe('ConfigureCases', () => { wrapper = mount(, { wrappingComponent: TestProviders }); }); - test('it submits the configuration correctly', () => { - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .simulate('click'); - + test('it submits the configuration correctly when changing connector', () => { + 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(persistCaseConfigure).toHaveBeenCalled(); @@ -479,382 +367,112 @@ describe('ConfigureCases', () => { }); }); - test('it has the correct url on cancel button', () => { - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('href') - ).toBe(`#/link-to/case${searchURL}`); - }); - - test('it disables the buttons of action bar when loading configuration', () => { - 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', - }, - loading: true, - })); - const newWrapper = mount(, { - wrappingComponent: TestProviders, - }); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - }); - }); - - describe('user interactions', () => { - beforeEach(() => { - jest.resetAllMocks(); - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-2', - connectorName: 'unchanged', - currentConfiguration: { - connectorName: 'unchanged', - connectorId: 'servicenow-2', - closureType: 'close-by-user', - }, - })); - useConnectorsMock.mockImplementation(() => useConnectorsResponse); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); - useGetUrlSearchMock.mockImplementation(() => searchURL); - }); - - test('it show the add flyout when pressing the add connector button', () => { - const wrapper = mount(, { wrappingComponent: TestProviders }); - wrapper - .find('button[data-test-subj="case-configure-add-connector-button"]') - .simulate('click'); - wrapper.update(); - - expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true); - 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-configure-update-selected-connector-button"]') - .simulate('click'); - wrapper.update(); - - expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(true); - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - }); - - test('it tracks the changes successfully', () => { - 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-pushing', - }, - })); - const wrapper = mount(, { wrappingComponent: TestProviders }); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); - wrapper.update(); - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() - .text() - ).toBe('2 unsaved changes'); - }); - - test('it tracks the changes successfully when name changes', () => { - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-2', - connectorName: 'nameChange', - currentConfiguration: { - connectorId: 'servicenow-1', - closureType: 'close-by-pushing', - connectorName: 'before', - }, - })); - - const wrapper = mount(, { wrappingComponent: TestProviders }); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); - wrapper.update(); - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() - .text() - ).toBe('2 unsaved changes'); - }); - - test('it tracks and reverts the changes successfully ', () => { - const wrapper = mount(, { wrappingComponent: TestProviders }); - // change settings - 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(); - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - // revert back to initial settings - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-servicenow-1"]').simulate('click'); - wrapper.update(); - wrapper.find('input[id="close-by-user"]').simulate('change'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - }); - - test('it close and restores the action bar when the add connector button is pressed', () => { - useCaseConfigureMock - .mockImplementationOnce(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-2', - currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, - })) - .mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-pushing', - connectorId: 'servicenow-2', - currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, - })); - const wrapper = mount(, { wrappingComponent: TestProviders }); - // Change closure type - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - - // Press add connector button - wrapper - .find('button[data-test-subj="case-configure-add-connector-button"]') - .simulate('click'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - - expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true); - - // Close the add flyout - wrapper.find('button[data-test-subj="euiFlyoutCloseButton"]').simulate('click'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - - expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(false); - - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() - .text() - ).toBe('1 unsaved changes'); - }); - - test('it close and restores the action bar when the update connector button is pressed', () => { + test('the text of the update button is changed successfully', () => { useCaseConfigureMock .mockImplementationOnce(() => ({ ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-2', - currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, + connectorId: 'servicenow-1', })) .mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-pushing', connectorId: 'servicenow-2', - currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, })); - const wrapper = mount(, { wrappingComponent: TestProviders }); - - // Change closure type - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - // Press update connector button - wrapper - .find('button[data-test-subj="case-configure-update-selected-connector-button"]') - .simulate('click'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - - expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(true); - - // Close the edit flyout - wrapper.find('button[data-test-subj="euiFlyoutCloseButton"]').simulate('click'); - wrapper.update(); - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - - expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(false); - - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() - .text() - ).toBe('1 unsaved changes'); - }); + wrapper = mount(, { wrappingComponent: TestProviders }); - test('it shows the action bar when the connector is changed', () => { - useCaseConfigureMock - .mockImplementationOnce(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[0].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-1', - currentConfiguration: { connectorId: 'servicenow-1', closureType: 'close-by-user' }, - })) - .mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-2', - currentConfiguration: { connectorId: 'servicenow-1', closureType: 'close-by-user' }, - })); - const wrapper = mount(, { wrappingComponent: TestProviders }); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); wrapper.update(); wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); wrapper.update(); - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); expect( wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') .text() - ).toBe('1 unsaved changes'); + ).toBe('Update My Connector 2'); }); + }); +}); - test('it closes the action bar when pressing save', () => { - useCaseConfigureMock - .mockImplementationOnce(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-2', - currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, - })) - .mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-pushing', - connectorId: 'servicenow-2', - currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, - })); - const wrapper = mount(, { wrappingComponent: TestProviders }); - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .simulate('click'); +describe('closure options', () => { + let wrapper: ReactWrapper; + const persistCaseConfigure = jest.fn(); + + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[0].config.casesConfiguration.mapping, + closureType: 'close-by-user', + connectorId: 'servicenow-1', + connectorName: 'My connector', + currentConfiguration: { + connectorName: 'My connector', + connectorId: 'My connector', + closureType: 'close-by-user', + }, + persistCaseConfigure, + })); + useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + + wrapper = mount(, { wrappingComponent: TestProviders }); + }); - wrapper.update(); + test('it submits the configuration correctly when changing closure type', () => { + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); + expect(persistCaseConfigure).toHaveBeenCalled(); + expect(persistCaseConfigure).toHaveBeenCalledWith({ + connectorId: 'servicenow-1', + connectorName: 'My Connector', + closureType: 'close-by-pushing', }); + }); +}); - test('the text of the update button is changed successfully', () => { - useCaseConfigureMock - .mockImplementationOnce(() => ({ - ...useCaseConfigureResponse, - connectorId: 'servicenow-1', - })) - .mockImplementation(() => ({ - ...useCaseConfigureResponse, - connectorId: 'servicenow-2', - })); +describe('user interactions', () => { + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[1].config.casesConfiguration.mapping, + closureType: 'close-by-user', + connectorId: 'servicenow-2', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: 'servicenow-2', + closureType: 'close-by-user', + }, + })); + useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + }); - const wrapper = mount(, { wrappingComponent: TestProviders }); + test('it show the add flyout when pressing the add connector button', () => { + 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-add-connector"]').simulate('click'); + wrapper.update(); - 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(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true); + }); - expect( - wrapper - .find('button[data-test-subj="case-configure-update-selected-connector-button"]') - .text() - ).toBe('Update My Connector 2'); - }); + test('it show the edit flyout when pressing the update connector button', () => { + const wrapper = mount(, { wrappingComponent: TestProviders }); + wrapper + .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('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); }); }); diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/index.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/index.tsx index d5c6cc671433b..e6fbc7cd3e4db 100644 --- a/x-pack/plugins/siem/public/cases/components/configure_cases/index.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/index.tsx @@ -7,16 +7,8 @@ import React, { useCallback, useEffect, useState, Dispatch, SetStateAction } from 'react'; import styled, { css } from 'styled-components'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiCallOut, - EuiBottomBar, - EuiButtonEmpty, - EuiText, -} from '@elastic/eui'; -import { difference } from 'lodash/fp'; +import { EuiCallOut } from '@elastic/eui'; + import { useKibana } from '../../../common/lib/kibana'; import { useConnectors } from '../../containers/configure/use_connectors'; import { useCaseConfigure } from '../../containers/configure/use_configure'; @@ -27,16 +19,15 @@ import { ConnectorEditFlyout, } from '../../../../../triggers_actions_ui/public'; +import { ClosureType } from '../../containers/configure/types'; + // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ActionConnectorTableItem } from '../../../../../triggers_actions_ui/public/types'; -import { getCaseUrl } from '../../../common/components/link_to'; -import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; import { connectorsConfiguration } from '../../../common/lib/connectors/config'; import { Connectors } from './connectors'; import { ClosureOptions } from './closure_options'; import { SectionWrapper } from '../wrappers'; -import { navTabs } from '../../../app/home/home_navigations'; import * as i18n from './translations'; const FormWrapper = styled.div` @@ -61,7 +52,6 @@ interface ConfigureCasesComponentProps { } const ConfigureCasesComponent: React.FC = ({ userCanCrud }) => { - const search = useGetUrlSearch(navTabs.case); const { http, triggers_actions_ui, notifications, application, docLinks } = useKibana().services; const [connectorIsValid, setConnectorIsValid] = useState(true); @@ -71,15 +61,13 @@ const ConfigureCasesComponent: React.FC = ({ userC null ); - const [actionBarVisible, setActionBarVisible] = useState(false); - const [totalConfigurationChanges, setTotalConfigurationChanges] = useState(0); - const { connectorId, closureType, currentConfiguration, loading: loadingCaseConfigure, persistLoading, + version, persistCaseConfigure, setConnector, setClosureType, @@ -93,45 +81,12 @@ const ConfigureCasesComponent: React.FC = ({ userC const isLoadingAny = isLoadingConnectors || persistLoading || loadingCaseConfigure; const updateConnectorDisabled = isLoadingAny || !connectorIsValid || connectorId === 'none'; - const handleSubmit = useCallback( - // TO DO give a warning/error to user when field are not mapped so they have chance to do it - () => { - setActionBarVisible(false); - persistCaseConfigure({ - connectorId, - connectorName: connectors.find(c => c.id === connectorId)?.name ?? '', - closureType, - }); - }, - [connectorId, connectors, closureType] - ); - - const onClickAddConnector = useCallback(() => { - setActionBarVisible(false); - setAddFlyoutVisibility(true); - }, []); - const onClickUpdateConnector = useCallback(() => { - setActionBarVisible(false); setEditFlyoutVisibility(true); }, []); - const handleActionBar = useCallback(() => { - const currentConfigurationMinusName = { - connectorId: currentConfiguration.connectorId, - closureType: currentConfiguration.closureType, - }; - const unsavedChanges = difference(Object.values(currentConfigurationMinusName), [ - connectorId, - closureType, - ]).length; - setActionBarVisible(!(unsavedChanges === 0)); - setTotalConfigurationChanges(unsavedChanges); - }, [currentConfiguration, connectorId, closureType]); - const handleSetAddFlyoutVisibility = useCallback( (isVisible: boolean) => { - handleActionBar(); setAddFlyoutVisibility(isVisible); }, [currentConfiguration, connectorId, closureType] @@ -139,12 +94,40 @@ const ConfigureCasesComponent: React.FC = ({ userC const handleSetEditFlyoutVisibility = useCallback( (isVisible: boolean) => { - handleActionBar(); setEditFlyoutVisibility(isVisible); }, [currentConfiguration, connectorId, closureType] ); + const onChangeConnector = useCallback( + (id: string) => { + if (id === 'add-connector') { + setAddFlyoutVisibility(true); + return; + } + + setConnector(id); + persistCaseConfigure({ + connectorId: id, + connectorName: connectors.find(c => c.id === id)?.name ?? '', + closureType, + }); + }, + [connectorId, closureType, version] + ); + + const onChangeClosureType = useCallback( + (type: ClosureType) => { + setClosureType(type); + persistCaseConfigure({ + connectorId, + connectorName: connectors.find(c => c.id === connectorId)?.name ?? '', + closureType: type, + }); + }, + [connectorId, closureType, version] + ); + useEffect(() => { if ( !isLoadingConnectors && @@ -168,16 +151,6 @@ const ConfigureCasesComponent: React.FC = ({ userC } }, [connectors, connectorId]); - useEffect(() => { - handleActionBar(); - }, [ - connectors, - connectorId, - closureType, - currentConfiguration.connectorId, - currentConfiguration.closureType, - ]); - return ( {!connectorIsValid && ( @@ -195,8 +168,8 @@ const ConfigureCasesComponent: React.FC = ({ userC @@ -204,57 +177,12 @@ const ConfigureCasesComponent: React.FC = ({ userC connectors={connectors ?? []} disabled={persistLoading || isLoadingConnectors || !userCanCrud} isLoading={isLoadingConnectors} - onChangeConnector={setConnector} + onChangeConnector={onChangeConnector} updateConnectorDisabled={updateConnectorDisabled || !userCanCrud} - handleShowAddFlyout={onClickAddConnector} handleShowEditFlyout={onClickUpdateConnector} selectedConnector={connectorId} /> - {actionBarVisible && ( - - - - - - {i18n.UNSAVED_CHANGES(totalConfigurationChanges)} - - - - - - - - {i18n.CANCEL} - - - - - {i18n.SAVE_CHANGES} - - - - - - - )} { defaultMessage: 'Update { connectorName }', }); }; - -export const UNSAVED_CHANGES = (unsavedChanges: number): string => { - return i18n.translate('xpack.siem.case.configureCases.unsavedChanges', { - values: { unsavedChanges }, - defaultMessage: '{unsavedChanges} unsaved changes', - }); -}; diff --git a/x-pack/plugins/siem/public/cases/components/edit_connector/index.test.tsx b/x-pack/plugins/siem/public/cases/components/edit_connector/index.test.tsx index 5dfed80baa8ed..8fada565b3463 100644 --- a/x-pack/plugins/siem/public/cases/components/edit_connector/index.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/edit_connector/index.test.tsx @@ -40,23 +40,17 @@ describe('EditConnector ', () => { ); - expect( - wrapper - .find(`[data-test-subj="dropdown-connectors"]`) - .last() - .prop('disabled') - ).toBeTruthy(); - expect( wrapper .find(`span[data-test-subj="dropdown-connector-no-connector"]`) .last() .exists() ).toBeTruthy(); - wrapper - .find(`[data-test-subj="connector-edit-button"]`) - .last() - .simulate('click'); + + 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 @@ -64,30 +58,27 @@ describe('EditConnector ', () => { .last() .exists() ).toBeTruthy(); - - expect( - wrapper - .find(`[data-test-subj="dropdown-connectors"]`) - .last() - .prop('disabled') - ).toBeFalsy(); }); + it('Edit external service on submit', async () => { const wrapper = mount( ); - wrapper - .find(`[data-test-subj="connector-edit-button"]`) - .last() - .simulate('click'); + + 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(`[data-test-subj="edit-connectors-submit"]`) .last() .exists() ).toBeTruthy(); + await act(async () => { wrapper .find(`[data-test-subj="edit-connectors-submit"]`) @@ -97,6 +88,7 @@ describe('EditConnector ', () => { expect(onSubmit).toBeCalledWith(sampleConnector); }); }); + it('Resets selector on cancel', async () => { const props = { ...defaultProps, @@ -106,10 +98,12 @@ describe('EditConnector ', () => { ); - wrapper - .find(`[data-test-subj="connector-edit-button"]`) - .last() - .simulate('click'); + + 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(); + await act(async () => { wrapper .find(`[data-test-subj="edit-connectors-cancel"]`) @@ -123,20 +117,7 @@ describe('EditConnector ', () => { ); }); }); - it('Renders disabled button', () => { - const props = { ...defaultProps, disabled: true }; - const wrapper = mount( - - - - ); - expect( - wrapper - .find(`[data-test-subj="connector-edit-button"]`) - .last() - .prop('disabled') - ).toBeTruthy(); - }); + it('Renders loading spinner', () => { const props = { ...defaultProps, isLoading: true }; const wrapper = mount( diff --git a/x-pack/plugins/siem/public/cases/components/edit_connector/index.tsx b/x-pack/plugins/siem/public/cases/components/edit_connector/index.tsx index 29f06532a4ab4..38062b8837054 100644 --- a/x-pack/plugins/siem/public/cases/components/edit_connector/index.tsx +++ b/x-pack/plugins/siem/public/cases/components/edit_connector/index.tsx @@ -12,7 +12,6 @@ import { EuiFlexItem, EuiButton, EuiButtonEmpty, - EuiButtonIcon, EuiLoadingSpinner, } from '@elastic/eui'; import styled, { css } from 'styled-components'; @@ -52,21 +51,24 @@ export const EditConnector = React.memo( options: { stripEmptyFields: false }, schema, }); - const [isEditConnector, setIsEditConnector] = useState(false); - const handleOnClick = useCallback(() => { - setIsEditConnector(true); - }, []); + const [connectorHasChanged, setConnectorHasChanged] = useState(false); + const onChangeConnector = useCallback( + connectorId => { + setConnectorHasChanged(selectedConnector !== connectorId); + }, + [selectedConnector] + ); const onCancelConnector = useCallback(() => { form.setFieldValue('connector', selectedConnector); - setIsEditConnector(false); + setConnectorHasChanged(false); }, [form, selectedConnector]); const onSubmitConnector = useCallback(async () => { const { isValid, data: newData } = await form.submit(); if (isValid && newData.connector) { onSubmit(newData.connector); - setIsEditConnector(false); + setConnectorHasChanged(false); } }, [form, onSubmit]); return ( @@ -76,17 +78,6 @@ export const EditConnector = React.memo(

{i18n.CONNECTORS}

{isLoading && } - {!isLoading && ( - - - - )} @@ -103,15 +94,16 @@ export const EditConnector = React.memo( dataTestSubj: 'caseConnectors', idAria: 'caseConnectors', isLoading, - disabled: !isEditConnector, + disabled, defaultValue: selectedConnector, }} + onChange={onChangeConnector} /> - {isEditConnector && ( + {connectorHasChanged && ( diff --git a/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.tsx b/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.tsx index ae8a67b75d36c..5d238b623eb4a 100644 --- a/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.tsx +++ b/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.tsx @@ -67,7 +67,11 @@ export const usePushToService = ({ }, [caseId, caseServices, caseConnectorId, caseConnectorName, postPushToService, updateCase]); const errorsMsg = useMemo(() => { - let errors: Array<{ title: string; description: JSX.Element }> = []; + let errors: Array<{ + title: string; + description: JSX.Element; + errorType?: 'primary' | 'success' | 'warning' | 'danger'; + }> = []; if (actionLicense != null && !actionLicense.enabledInLicense) { errors = [...errors, getLicenseError()]; } @@ -115,6 +119,7 @@ export const usePushToService = ({ id="xpack.siem.case.caseView.pushToServiceDisableByInvalidConnector" /> ), + errorType: 'danger', }, ]; } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 219e922a0158c..da47e42ea02a6 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -13152,7 +13152,6 @@ "xpack.siem.case.configureCases.incidentManagementSystemLabel": "インシデント管理システム", "xpack.siem.case.configureCases.incidentManagementSystemTitle": "サードパーティのインシデント管理システムに接続", "xpack.siem.case.configureCases.noConnector": "コネクターを選択していません", - "xpack.siem.case.configureCases.saveChangesButton": "変更を保存", "xpack.siem.case.configureCases.updateConnector": "コネクターを更新", "xpack.siem.case.configureCases.warningMessage": "構成が無効のようです。選択したコネクターが見つかりませんコネクターを削除しましたか?", "xpack.siem.case.configureCases.warningTitle": "警告", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 954162647bf83..3d96a0ebe0a3f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13159,7 +13159,6 @@ "xpack.siem.case.configureCases.incidentManagementSystemLabel": "事件管理系统", "xpack.siem.case.configureCases.incidentManagementSystemTitle": "连接到第三方事件管理系统", "xpack.siem.case.configureCases.noConnector": "未选择连接器", - "xpack.siem.case.configureCases.saveChangesButton": "保存更改", "xpack.siem.case.configureCases.updateConnector": "更新连接器", "xpack.siem.case.configureCases.warningMessage": "配置似乎无效。选择的连接器缺失。您是否已删除该连接器?", "xpack.siem.case.configureCases.warningTitle": "警告",