Skip to content

Commit 2913fef

Browse files
authored
[7.x] [Security Solution][Case] Fix connector's dropdown with conflicting requests (#72037) (#72261)
1 parent 3f5e207 commit 2913fef

File tree

13 files changed

+154
-60
lines changed

13 files changed

+154
-60
lines changed

x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,4 +409,32 @@ describe('CaseView ', () => {
409409
wrapper.find('button[data-test-subj="push-to-external-service"]').first().prop('disabled')
410410
).toBeTruthy();
411411
});
412+
413+
it('should revert to the initial connector in case of failure', async () => {
414+
updateCaseProperty.mockImplementation(({ onError }) => {
415+
onError();
416+
});
417+
const wrapper = mount(
418+
<TestProviders>
419+
<Router history={mockHistory}>
420+
<CaseComponent
421+
{...caseProps}
422+
caseData={{ ...caseProps.caseData, connectorId: 'servicenow-1' }}
423+
/>
424+
</Router>
425+
</TestProviders>
426+
);
427+
await wait();
428+
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
429+
wrapper.update();
430+
wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click');
431+
wrapper.update();
432+
wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click');
433+
wrapper.update();
434+
await wait();
435+
wrapper.update();
436+
expect(
437+
wrapper.find('[data-test-subj="dropdown-connectors"]').at(0).prop('valueOfSelected')
438+
).toBe('servicenow-1');
439+
});
412440
});

x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ interface Props {
4444
userCanCrud: boolean;
4545
}
4646

47+
export interface OnUpdateFields {
48+
key: keyof Case;
49+
value: Case[keyof Case];
50+
onSuccess?: () => void;
51+
onError?: () => void;
52+
}
53+
4754
const MyWrapper = styled.div`
4855
padding: ${({
4956
theme,
@@ -88,65 +95,75 @@ export const CaseComponent = React.memo<CaseProps>(
8895

8996
// Update Fields
9097
const onUpdateField = useCallback(
91-
(newUpdateKey: keyof Case, updateValue: Case[keyof Case]) => {
98+
({ key, value, onSuccess, onError }: OnUpdateFields) => {
9299
const handleUpdateNewCase = (newCase: Case) =>
93100
updateCase({ ...newCase, comments: caseData.comments });
94-
switch (newUpdateKey) {
101+
switch (key) {
95102
case 'title':
96-
const titleUpdate = getTypedPayload<string>(updateValue);
103+
const titleUpdate = getTypedPayload<string>(value);
97104
if (titleUpdate.length > 0) {
98105
updateCaseProperty({
99106
fetchCaseUserActions,
100107
updateKey: 'title',
101108
updateValue: titleUpdate,
102109
updateCase: handleUpdateNewCase,
103110
version: caseData.version,
111+
onSuccess,
112+
onError,
104113
});
105114
}
106115
break;
107116
case 'connectorId':
108-
const connectorId = getTypedPayload<string>(updateValue);
117+
const connectorId = getTypedPayload<string>(value);
109118
if (connectorId.length > 0) {
110119
updateCaseProperty({
111120
fetchCaseUserActions,
112121
updateKey: 'connector_id',
113122
updateValue: connectorId,
114123
updateCase: handleUpdateNewCase,
115124
version: caseData.version,
125+
onSuccess,
126+
onError,
116127
});
117128
}
118129
break;
119130
case 'description':
120-
const descriptionUpdate = getTypedPayload<string>(updateValue);
131+
const descriptionUpdate = getTypedPayload<string>(value);
121132
if (descriptionUpdate.length > 0) {
122133
updateCaseProperty({
123134
fetchCaseUserActions,
124135
updateKey: 'description',
125136
updateValue: descriptionUpdate,
126137
updateCase: handleUpdateNewCase,
127138
version: caseData.version,
139+
onSuccess,
140+
onError,
128141
});
129142
}
130143
break;
131144
case 'tags':
132-
const tagsUpdate = getTypedPayload<string[]>(updateValue);
145+
const tagsUpdate = getTypedPayload<string[]>(value);
133146
updateCaseProperty({
134147
fetchCaseUserActions,
135148
updateKey: 'tags',
136149
updateValue: tagsUpdate,
137150
updateCase: handleUpdateNewCase,
138151
version: caseData.version,
152+
onSuccess,
153+
onError,
139154
});
140155
break;
141156
case 'status':
142-
const statusUpdate = getTypedPayload<string>(updateValue);
143-
if (caseData.status !== updateValue) {
157+
const statusUpdate = getTypedPayload<string>(value);
158+
if (caseData.status !== value) {
144159
updateCaseProperty({
145160
fetchCaseUserActions,
146161
updateKey: 'status',
147162
updateValue: statusUpdate,
148163
updateCase: handleUpdateNewCase,
149164
version: caseData.version,
165+
onSuccess,
166+
onError,
150167
});
151168
}
152169
default:
@@ -191,15 +208,28 @@ export const CaseComponent = React.memo<CaseProps>(
191208
});
192209

193210
const onSubmitConnector = useCallback(
194-
(connectorId) => onUpdateField('connectorId', connectorId),
211+
(connectorId, onSuccess, onError) =>
212+
onUpdateField({
213+
key: 'connectorId',
214+
value: connectorId,
215+
onSuccess,
216+
onError,
217+
}),
195218
[onUpdateField]
196219
);
197-
const onSubmitTags = useCallback((newTags) => onUpdateField('tags', newTags), [onUpdateField]);
198-
const onSubmitTitle = useCallback((newTitle) => onUpdateField('title', newTitle), [
220+
const onSubmitTags = useCallback((newTags) => onUpdateField({ key: 'tags', value: newTags }), [
199221
onUpdateField,
200222
]);
223+
const onSubmitTitle = useCallback(
224+
(newTitle) => onUpdateField({ key: 'title', value: newTitle }),
225+
[onUpdateField]
226+
);
201227
const toggleStatusCase = useCallback(
202-
(e) => onUpdateField('status', e.target.checked ? 'closed' : 'open'),
228+
(e) =>
229+
onUpdateField({
230+
key: 'status',
231+
value: e.target.checked ? 'closed' : 'open',
232+
}),
203233
[onUpdateField]
204234
);
205235
const handleRefresh = useCallback(() => {

x-pack/plugins/security_solution/public/cases/components/edit_connector/index.test.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,35 @@ describe('EditConnector ', () => {
6969
await act(async () => {
7070
wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click');
7171
await wait();
72-
expect(onSubmit).toBeCalledWith(sampleConnector);
72+
expect(onSubmit.mock.calls[0][0]).toBe(sampleConnector);
7373
});
7474
});
7575

76+
it('Revert to initial external service on error', async () => {
77+
onSubmit.mockImplementation((connector, onSuccess, onError) => {
78+
onError(new Error('An error has occurred'));
79+
});
80+
const wrapper = mount(
81+
<TestProviders>
82+
<EditConnector {...defaultProps} />
83+
</TestProviders>
84+
);
85+
86+
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
87+
wrapper.update();
88+
wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click');
89+
wrapper.update();
90+
91+
expect(wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().exists()).toBeTruthy();
92+
93+
await act(async () => {
94+
wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click');
95+
await wait();
96+
wrapper.update();
97+
});
98+
expect(formHookMock.setFieldValue).toHaveBeenCalledWith('connector', 'none');
99+
});
100+
76101
it('Resets selector on cancel', async () => {
77102
const props = {
78103
...defaultProps,

x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
EuiLoadingSpinner,
1616
} from '@elastic/eui';
1717
import styled, { css } from 'styled-components';
18+
import { noop } from 'lodash/fp';
19+
1820
import * as i18n from '../../translations';
1921
import { Form, UseField, useForm } from '../../../shared_imports';
2022
import { schema } from './schema';
@@ -25,7 +27,7 @@ interface EditConnectorProps {
2527
connectors: Connector[];
2628
disabled?: boolean;
2729
isLoading: boolean;
28-
onSubmit: (a: string[]) => void;
30+
onSubmit: (a: string[], onSuccess: () => void, onError: () => void) => void;
2931
selectedConnector: string;
3032
}
3133

@@ -61,6 +63,11 @@ export const EditConnector = React.memo(
6163
[selectedConnector]
6264
);
6365

66+
const onError = useCallback(() => {
67+
setFieldValue('connector', selectedConnector);
68+
setConnectorHasChanged(false);
69+
}, [setFieldValue, selectedConnector]);
70+
6471
const onCancelConnector = useCallback(() => {
6572
setFieldValue('connector', selectedConnector);
6673
setConnectorHasChanged(false);
@@ -69,10 +76,10 @@ export const EditConnector = React.memo(
6976
const onSubmitConnector = useCallback(async () => {
7077
const { isValid, data: newData } = await submit();
7178
if (isValid && newData.connector) {
72-
onSubmit(newData.connector);
79+
onSubmit(newData.connector, noop, onError);
7380
setConnectorHasChanged(false);
7481
}
75-
}, [submit, onSubmit]);
82+
}, [submit, onSubmit, onError]);
7683

7784
return (
7885
<EuiText>

x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ describe('UserActionTree ', () => {
277277
)
278278
.exists()
279279
).toEqual(false);
280-
expect(onUpdateField).toBeCalledWith('description', sampleData.content);
280+
expect(onUpdateField).toBeCalledWith({ key: 'description', value: sampleData.content });
281281
});
282282
});
283283

x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { UserActionMarkdown } from './user_action_markdown';
2121
import { Connector } from '../../../../../case/common/api/cases';
2222
import { CaseServices } from '../../containers/use_get_case_user_actions';
2323
import { parseString } from '../../containers/utils';
24+
import { OnUpdateFields } from '../case_view';
2425

2526
export interface UserActionTreeProps {
2627
caseServices: CaseServices;
@@ -30,7 +31,7 @@ export interface UserActionTreeProps {
3031
fetchUserActions: () => void;
3132
isLoadingDescription: boolean;
3233
isLoadingUserActions: boolean;
33-
onUpdateField: (updateKey: keyof Case, updateValue: string | string[]) => void;
34+
onUpdateField: ({ key, value, onSuccess, onError }: OnUpdateFields) => void;
3435
updateCase: (newCase: Case) => void;
3536
userCanCrud: boolean;
3637
}
@@ -138,7 +139,7 @@ export const UserActionTree = React.memo(
138139
content={caseData.description}
139140
isEditable={manageMarkdownEditIds.includes(DESCRIPTION_ID)}
140141
onSaveContent={(content: string) => {
141-
onUpdateField(DESCRIPTION_ID, content);
142+
onUpdateField({ key: DESCRIPTION_ID, value: content });
142143
}}
143144
onChangeEditable={handleManageMarkdownEditId}
144145
/>

x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@ describe('useUpdateCase', () => {
1616
const fetchCaseUserActions = jest.fn();
1717
const updateCase = jest.fn();
1818
const updateKey: UpdateKey = 'description';
19+
const onSuccess = jest.fn();
20+
const onError = jest.fn();
21+
1922
const sampleUpdate = {
2023
fetchCaseUserActions,
2124
updateKey,
2225
updateValue: 'updated description',
2326
updateCase,
2427
version: basicCase.version,
28+
onSuccess,
29+
onError,
2530
};
2631
beforeEach(() => {
2732
jest.clearAllMocks();
@@ -79,6 +84,7 @@ describe('useUpdateCase', () => {
7984
});
8085
expect(fetchCaseUserActions).toBeCalledWith(basicCase.id);
8186
expect(updateCase).toBeCalledWith(basicCase);
87+
expect(onSuccess).toHaveBeenCalled();
8288
});
8389
});
8490

@@ -114,6 +120,7 @@ describe('useUpdateCase', () => {
114120
isError: true,
115121
updateCaseProperty: result.current.updateCaseProperty,
116122
});
123+
expect(onError).toHaveBeenCalled();
117124
});
118125
});
119126
});

x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import { useReducer, useCallback } from 'react';
8+
89
import {
910
displaySuccessToast,
1011
errorToToaster,
@@ -33,6 +34,8 @@ export interface UpdateByKey {
3334
fetchCaseUserActions?: (caseId: string) => void;
3435
updateCase?: (newCase: Case) => void;
3536
version: string;
37+
onSuccess?: () => void;
38+
onError?: () => void;
3639
}
3740

3841
type Action =
@@ -81,7 +84,15 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase =>
8184
const [, dispatchToaster] = useStateToaster();
8285

8386
const dispatchUpdateCaseProperty = useCallback(
84-
async ({ fetchCaseUserActions, updateKey, updateValue, updateCase, version }: UpdateByKey) => {
87+
async ({
88+
fetchCaseUserActions,
89+
updateKey,
90+
updateValue,
91+
updateCase,
92+
version,
93+
onSuccess,
94+
onError,
95+
}: UpdateByKey) => {
8596
let cancel = false;
8697
const abortCtrl = new AbortController();
8798

@@ -102,6 +113,9 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase =>
102113
}
103114
dispatch({ type: 'FETCH_SUCCESS' });
104115
displaySuccessToast(i18n.UPDATED_CASE(response[0].title), dispatchToaster);
116+
if (onSuccess) {
117+
onSuccess();
118+
}
105119
}
106120
} catch (error) {
107121
if (!cancel) {
@@ -111,6 +125,9 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase =>
111125
dispatchToaster,
112126
});
113127
dispatch({ type: 'FETCH_FAILURE' });
128+
if (onError) {
129+
onError();
130+
}
114131
}
115132
}
116133
return () => {

0 commit comments

Comments
 (0)