Revert "[Security Solution][Exceptions] - Improve UX for missing exception list associated with rule (#75898)"
This reverts commit b9c8201202
.
This commit is contained in:
parent
d6c45a2e70
commit
e773f221a3
|
@ -18,6 +18,7 @@ import {
|
|||
EuiCheckbox,
|
||||
EuiSpacer,
|
||||
EuiFormRow,
|
||||
EuiCallOut,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { Status } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
|
@ -27,7 +28,6 @@ import {
|
|||
ExceptionListType,
|
||||
} from '../../../../../public/lists_plugin_deps';
|
||||
import * as i18n from './translations';
|
||||
import * as sharedI18n from '../translations';
|
||||
import { TimelineNonEcsData, Ecs } from '../../../../graphql/types';
|
||||
import { useAppToasts } from '../../../hooks/use_app_toasts';
|
||||
import { useKibana } from '../../../lib/kibana';
|
||||
|
@ -35,7 +35,6 @@ import { ExceptionBuilderComponent } from '../builder';
|
|||
import { Loader } from '../../loader';
|
||||
import { useAddOrUpdateException } from '../use_add_exception';
|
||||
import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index';
|
||||
import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async';
|
||||
import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list';
|
||||
import { AddExceptionComments } from '../add_exception_comments';
|
||||
import {
|
||||
|
@ -47,7 +46,6 @@ import {
|
|||
entryHasNonEcsType,
|
||||
getMappedNonEcsValue,
|
||||
} from '../helpers';
|
||||
import { ErrorInfo, ErrorCallout } from '../error_callout';
|
||||
import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules';
|
||||
|
||||
export interface AddExceptionModalBaseProps {
|
||||
|
@ -109,14 +107,13 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
}: AddExceptionModalProps) {
|
||||
const { http } = useKibana().services;
|
||||
const [comment, setComment] = useState('');
|
||||
const { rule: maybeRule } = useRuleAsync(ruleId);
|
||||
const [shouldCloseAlert, setShouldCloseAlert] = useState(false);
|
||||
const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false);
|
||||
const [shouldDisableBulkClose, setShouldDisableBulkClose] = useState(false);
|
||||
const [exceptionItemsToAdd, setExceptionItemsToAdd] = useState<
|
||||
Array<ExceptionListItemSchema | CreateExceptionListItemSchema>
|
||||
>([]);
|
||||
const [fetchOrCreateListError, setFetchOrCreateListError] = useState<ErrorInfo | null>(null);
|
||||
const [fetchOrCreateListError, setFetchOrCreateListError] = useState(false);
|
||||
const { addError, addSuccess } = useAppToasts();
|
||||
const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex();
|
||||
const [
|
||||
|
@ -167,41 +164,17 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
},
|
||||
[onRuleChange]
|
||||
);
|
||||
|
||||
const handleDissasociationSuccess = useCallback(
|
||||
(id: string): void => {
|
||||
handleRuleChange(true);
|
||||
addSuccess(sharedI18n.DISSASOCIATE_LIST_SUCCESS(id));
|
||||
onCancel();
|
||||
},
|
||||
[handleRuleChange, addSuccess, onCancel]
|
||||
);
|
||||
|
||||
const handleDissasociationError = useCallback(
|
||||
(error: Error): void => {
|
||||
addError(error, { title: sharedI18n.DISSASOCIATE_EXCEPTION_LIST_ERROR });
|
||||
onCancel();
|
||||
},
|
||||
[addError, onCancel]
|
||||
);
|
||||
|
||||
const handleFetchOrCreateExceptionListError = useCallback(
|
||||
(error: Error, statusCode: number | null, message: string | null) => {
|
||||
setFetchOrCreateListError({
|
||||
reason: error.message,
|
||||
code: statusCode,
|
||||
details: message,
|
||||
listListId: null,
|
||||
});
|
||||
const onFetchOrCreateExceptionListError = useCallback(
|
||||
(error: Error) => {
|
||||
setFetchOrCreateListError(true);
|
||||
},
|
||||
[setFetchOrCreateListError]
|
||||
);
|
||||
|
||||
const [isLoadingExceptionList, ruleExceptionList] = useFetchOrCreateRuleExceptionList({
|
||||
http,
|
||||
ruleId,
|
||||
exceptionListType,
|
||||
onError: handleFetchOrCreateExceptionListError,
|
||||
onError: onFetchOrCreateExceptionListError,
|
||||
onSuccess: handleRuleChange,
|
||||
});
|
||||
|
||||
|
@ -306,9 +279,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
]);
|
||||
|
||||
const isSubmitButtonDisabled = useMemo(
|
||||
() =>
|
||||
fetchOrCreateListError != null ||
|
||||
exceptionItemsToAdd.every((item) => item.entries.length === 0),
|
||||
() => fetchOrCreateListError || exceptionItemsToAdd.every((item) => item.entries.length === 0),
|
||||
[fetchOrCreateListError, exceptionItemsToAdd]
|
||||
);
|
||||
|
||||
|
@ -324,27 +295,19 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
</ModalHeaderSubtitle>
|
||||
</ModalHeader>
|
||||
|
||||
{fetchOrCreateListError != null && (
|
||||
<EuiModalFooter>
|
||||
<ErrorCallout
|
||||
http={http}
|
||||
errorInfo={fetchOrCreateListError}
|
||||
rule={maybeRule}
|
||||
onCancel={onCancel}
|
||||
onSuccess={handleDissasociationSuccess}
|
||||
onError={handleDissasociationError}
|
||||
data-test-subj="addExceptionModalErrorCallout"
|
||||
/>
|
||||
</EuiModalFooter>
|
||||
{fetchOrCreateListError === true && (
|
||||
<EuiCallOut title={i18n.ADD_EXCEPTION_FETCH_ERROR_TITLE} color="danger" iconType="alert">
|
||||
<p>{i18n.ADD_EXCEPTION_FETCH_ERROR}</p>
|
||||
</EuiCallOut>
|
||||
)}
|
||||
{fetchOrCreateListError == null &&
|
||||
{fetchOrCreateListError === false &&
|
||||
(isLoadingExceptionList ||
|
||||
isIndexPatternLoading ||
|
||||
isSignalIndexLoading ||
|
||||
isSignalIndexPatternLoading) && (
|
||||
<Loader data-test-subj="loadingAddExceptionModal" size="xl" />
|
||||
)}
|
||||
{fetchOrCreateListError == null &&
|
||||
{fetchOrCreateListError === false &&
|
||||
!isSignalIndexLoading &&
|
||||
!isSignalIndexPatternLoading &&
|
||||
!isLoadingExceptionList &&
|
||||
|
@ -414,21 +377,20 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
</ModalBodySection>
|
||||
</>
|
||||
)}
|
||||
{fetchOrCreateListError == null && (
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={onCancel}>{i18n.CANCEL}</EuiButtonEmpty>
|
||||
|
||||
<EuiButton
|
||||
data-test-subj="add-exception-confirm-button"
|
||||
onClick={onAddExceptionConfirm}
|
||||
isLoading={addExceptionIsLoading}
|
||||
isDisabled={isSubmitButtonDisabled}
|
||||
fill
|
||||
>
|
||||
{i18n.ADD_EXCEPTION}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
)}
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={onCancel}>{i18n.CANCEL}</EuiButtonEmpty>
|
||||
|
||||
<EuiButton
|
||||
data-test-subj="add-exception-confirm-button"
|
||||
onClick={onAddExceptionConfirm}
|
||||
isLoading={addExceptionIsLoading}
|
||||
isDisabled={isSubmitButtonDisabled}
|
||||
fill
|
||||
>
|
||||
{i18n.ADD_EXCEPTION}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</Modal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
|
|
|
@ -77,7 +77,6 @@ describe('When the edit exception modal is opened', () => {
|
|||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<EditExceptionModal
|
||||
ruleIndices={[]}
|
||||
ruleId="123"
|
||||
ruleName={ruleName}
|
||||
exceptionListType={'endpoint'}
|
||||
onCancel={jest.fn()}
|
||||
|
@ -106,7 +105,6 @@ describe('When the edit exception modal is opened', () => {
|
|||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<EditExceptionModal
|
||||
ruleIndices={['filebeat-*']}
|
||||
ruleId="123"
|
||||
ruleName={ruleName}
|
||||
exceptionListType={'endpoint'}
|
||||
onCancel={jest.fn()}
|
||||
|
@ -149,7 +147,6 @@ describe('When the edit exception modal is opened', () => {
|
|||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<EditExceptionModal
|
||||
ruleIndices={['filebeat-*']}
|
||||
ruleId="123"
|
||||
ruleName={ruleName}
|
||||
exceptionListType={'endpoint'}
|
||||
onCancel={jest.fn()}
|
||||
|
@ -193,7 +190,6 @@ describe('When the edit exception modal is opened', () => {
|
|||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<EditExceptionModal
|
||||
ruleIndices={['filebeat-*']}
|
||||
ruleId="123"
|
||||
ruleName={ruleName}
|
||||
exceptionListType={'detection'}
|
||||
onCancel={jest.fn()}
|
||||
|
@ -233,7 +229,6 @@ describe('When the edit exception modal is opened', () => {
|
|||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<EditExceptionModal
|
||||
ruleIndices={['filebeat-*']}
|
||||
ruleId="123"
|
||||
ruleName={ruleName}
|
||||
exceptionListType={'detection'}
|
||||
onCancel={jest.fn()}
|
||||
|
|
|
@ -23,14 +23,12 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules';
|
||||
import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index';
|
||||
import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async';
|
||||
import {
|
||||
ExceptionListItemSchema,
|
||||
CreateExceptionListItemSchema,
|
||||
ExceptionListType,
|
||||
} from '../../../../../public/lists_plugin_deps';
|
||||
import * as i18n from './translations';
|
||||
import * as sharedI18n from '../translations';
|
||||
import { useKibana } from '../../../lib/kibana';
|
||||
import { useAppToasts } from '../../../hooks/use_app_toasts';
|
||||
import { ExceptionBuilderComponent } from '../builder';
|
||||
|
@ -45,17 +43,14 @@ import {
|
|||
lowercaseHashValues,
|
||||
} from '../helpers';
|
||||
import { Loader } from '../../loader';
|
||||
import { ErrorInfo, ErrorCallout } from '../error_callout';
|
||||
|
||||
interface EditExceptionModalProps {
|
||||
ruleName: string;
|
||||
ruleId: string;
|
||||
ruleIndices: string[];
|
||||
exceptionItem: ExceptionListItemSchema;
|
||||
exceptionListType: ExceptionListType;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
onRuleChange?: () => void;
|
||||
}
|
||||
|
||||
const Modal = styled(EuiModal)`
|
||||
|
@ -88,18 +83,14 @@ const ModalBodySection = styled.section`
|
|||
|
||||
export const EditExceptionModal = memo(function EditExceptionModal({
|
||||
ruleName,
|
||||
ruleId,
|
||||
ruleIndices,
|
||||
exceptionItem,
|
||||
exceptionListType,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
onRuleChange,
|
||||
}: EditExceptionModalProps) {
|
||||
const { http } = useKibana().services;
|
||||
const [comment, setComment] = useState('');
|
||||
const { rule: maybeRule } = useRuleAsync(ruleId);
|
||||
const [updateError, setUpdateError] = useState<ErrorInfo | null>(null);
|
||||
const [hasVersionConflict, setHasVersionConflict] = useState(false);
|
||||
const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false);
|
||||
const [shouldDisableBulkClose, setShouldDisableBulkClose] = useState(false);
|
||||
|
@ -117,44 +108,18 @@ export const EditExceptionModal = memo(function EditExceptionModal({
|
|||
'rules'
|
||||
);
|
||||
|
||||
const handleExceptionUpdateError = useCallback(
|
||||
(error: Error, statusCode: number | null, message: string | null) => {
|
||||
const onError = useCallback(
|
||||
(error) => {
|
||||
if (error.message.includes('Conflict')) {
|
||||
setHasVersionConflict(true);
|
||||
} else {
|
||||
setUpdateError({
|
||||
reason: error.message,
|
||||
code: statusCode,
|
||||
details: message,
|
||||
listListId: exceptionItem.list_id,
|
||||
});
|
||||
addError(error, { title: i18n.EDIT_EXCEPTION_ERROR });
|
||||
onCancel();
|
||||
}
|
||||
},
|
||||
[setUpdateError, setHasVersionConflict, exceptionItem.list_id]
|
||||
);
|
||||
|
||||
const handleDissasociationSuccess = useCallback(
|
||||
(id: string): void => {
|
||||
addSuccess(sharedI18n.DISSASOCIATE_LIST_SUCCESS(id));
|
||||
|
||||
if (onRuleChange) {
|
||||
onRuleChange();
|
||||
}
|
||||
|
||||
onCancel();
|
||||
},
|
||||
[addSuccess, onCancel, onRuleChange]
|
||||
);
|
||||
|
||||
const handleDissasociationError = useCallback(
|
||||
(error: Error): void => {
|
||||
addError(error, { title: sharedI18n.DISSASOCIATE_EXCEPTION_LIST_ERROR });
|
||||
onCancel();
|
||||
},
|
||||
[addError, onCancel]
|
||||
);
|
||||
|
||||
const handleExceptionUpdateSuccess = useCallback((): void => {
|
||||
const onSuccess = useCallback(() => {
|
||||
addSuccess(i18n.EDIT_EXCEPTION_SUCCESS);
|
||||
onConfirm();
|
||||
}, [addSuccess, onConfirm]);
|
||||
|
@ -162,8 +127,8 @@ export const EditExceptionModal = memo(function EditExceptionModal({
|
|||
const [{ isLoading: addExceptionIsLoading }, addOrUpdateExceptionItems] = useAddOrUpdateException(
|
||||
{
|
||||
http,
|
||||
onSuccess: handleExceptionUpdateSuccess,
|
||||
onError: handleExceptionUpdateError,
|
||||
onSuccess,
|
||||
onError,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -257,9 +222,11 @@ export const EditExceptionModal = memo(function EditExceptionModal({
|
|||
{ruleName}
|
||||
</ModalHeaderSubtitle>
|
||||
</ModalHeader>
|
||||
|
||||
{(addExceptionIsLoading || isIndexPatternLoading || isSignalIndexLoading) && (
|
||||
<Loader data-test-subj="loadingEditExceptionModal" size="xl" />
|
||||
)}
|
||||
|
||||
{!isSignalIndexLoading && !addExceptionIsLoading && !isIndexPatternLoading && (
|
||||
<>
|
||||
<ModalBodySection className="builder-section">
|
||||
|
@ -313,18 +280,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({
|
|||
</ModalBodySection>
|
||||
</>
|
||||
)}
|
||||
{updateError != null && (
|
||||
<ModalBodySection>
|
||||
<ErrorCallout
|
||||
http={http}
|
||||
errorInfo={updateError}
|
||||
rule={maybeRule}
|
||||
onCancel={onCancel}
|
||||
onSuccess={handleDissasociationSuccess}
|
||||
onError={handleDissasociationError}
|
||||
/>
|
||||
</ModalBodySection>
|
||||
)}
|
||||
|
||||
{hasVersionConflict && (
|
||||
<ModalBodySection>
|
||||
<EuiCallOut title={i18n.VERSION_CONFLICT_ERROR_TITLE} color="danger" iconType="alert">
|
||||
|
@ -332,21 +288,20 @@ export const EditExceptionModal = memo(function EditExceptionModal({
|
|||
</EuiCallOut>
|
||||
</ModalBodySection>
|
||||
)}
|
||||
{updateError == null && (
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={onCancel}>{i18n.CANCEL}</EuiButtonEmpty>
|
||||
|
||||
<EuiButton
|
||||
data-test-subj="edit-exception-confirm-button"
|
||||
onClick={onEditExceptionConfirm}
|
||||
isLoading={addExceptionIsLoading}
|
||||
isDisabled={isSubmitButtonDisabled}
|
||||
fill
|
||||
>
|
||||
{i18n.EDIT_EXCEPTION_SAVE_BUTTON}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
)}
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={onCancel}>{i18n.CANCEL}</EuiButtonEmpty>
|
||||
|
||||
<EuiButton
|
||||
data-test-subj="edit-exception-confirm-button"
|
||||
onClick={onEditExceptionConfirm}
|
||||
isLoading={addExceptionIsLoading}
|
||||
isDisabled={isSubmitButtonDisabled}
|
||||
fill
|
||||
>
|
||||
{i18n.EDIT_EXCEPTION_SAVE_BUTTON}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</Modal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
|
|
|
@ -1,160 +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 { ThemeProvider } from 'styled-components';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||
|
||||
import { getListMock } from '../../../../common/detection_engine/schemas/types/lists.mock';
|
||||
import { useDissasociateExceptionList } from '../../../detections/containers/detection_engine/rules/use_dissasociate_exception_list';
|
||||
import { createKibanaCoreStartMock } from '../../mock/kibana_core';
|
||||
import { ErrorCallout } from './error_callout';
|
||||
import { savedRuleMock } from '../../../detections/containers/detection_engine/rules/mock';
|
||||
|
||||
jest.mock('../../../detections/containers/detection_engine/rules/use_dissasociate_exception_list');
|
||||
|
||||
const mockKibanaHttpService = createKibanaCoreStartMock().http;
|
||||
|
||||
describe('ErrorCallout', () => {
|
||||
const mockDissasociate = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
(useDissasociateExceptionList as jest.Mock).mockReturnValue([false, mockDissasociate]);
|
||||
});
|
||||
|
||||
it('it renders error details', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ErrorCallout
|
||||
http={mockKibanaHttpService}
|
||||
errorInfo={{
|
||||
reason: 'error reason',
|
||||
code: 500,
|
||||
details: null,
|
||||
listListId: null,
|
||||
}}
|
||||
rule={{ ...savedRuleMock, exceptions_list: [getListMock()] }}
|
||||
onCancel={jest.fn()}
|
||||
onSuccess={jest.fn()}
|
||||
onError={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="errorCalloutContainer"] .euiCallOutHeader__title').text()
|
||||
).toEqual('Error: error reason (500)');
|
||||
expect(wrapper.find('[data-test-subj="errorCalloutMessage"]').at(0).text()).toEqual(
|
||||
'Error fetching exception list'
|
||||
);
|
||||
});
|
||||
|
||||
it('it invokes "onCancel" when cancel button clicked', () => {
|
||||
const mockOnCancel = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ErrorCallout
|
||||
http={mockKibanaHttpService}
|
||||
errorInfo={{
|
||||
reason: 'error reason',
|
||||
code: 500,
|
||||
details: null,
|
||||
listListId: null,
|
||||
}}
|
||||
rule={{ ...savedRuleMock, exceptions_list: [getListMock()] }}
|
||||
onCancel={mockOnCancel}
|
||||
onSuccess={jest.fn()}
|
||||
onError={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="errorCalloutCancelButton"]').at(0).simulate('click');
|
||||
|
||||
expect(mockOnCancel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('it does not render status code if not available', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ErrorCallout
|
||||
http={mockKibanaHttpService}
|
||||
errorInfo={{
|
||||
reason: 'not found',
|
||||
code: null,
|
||||
details: 'list of id "some_uuid" not found',
|
||||
listListId: null,
|
||||
}}
|
||||
rule={{ ...savedRuleMock, exceptions_list: [getListMock()] }}
|
||||
onCancel={jest.fn()}
|
||||
onSuccess={jest.fn()}
|
||||
onError={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="errorCalloutContainer"] .euiCallOutHeader__title').text()
|
||||
).toEqual('Error: not found');
|
||||
expect(wrapper.find('[data-test-subj="errorCalloutMessage"]').at(0).text()).toEqual(
|
||||
'Error fetching exception list'
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('it renders specific missing exceptions list error', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ErrorCallout
|
||||
http={mockKibanaHttpService}
|
||||
errorInfo={{
|
||||
reason: 'not found',
|
||||
code: 404,
|
||||
details: 'list of id "some_uuid" not found',
|
||||
listListId: null,
|
||||
}}
|
||||
rule={{ ...savedRuleMock, exceptions_list: [getListMock()] }}
|
||||
onCancel={jest.fn()}
|
||||
onSuccess={jest.fn()}
|
||||
onError={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="errorCalloutContainer"] .euiCallOutHeader__title').text()
|
||||
).toEqual('Error: not found (404)');
|
||||
expect(wrapper.find('[data-test-subj="errorCalloutMessage"]').at(0).text()).toEqual(
|
||||
'The associated exception list (some_uuid) no longer exists. Please remove the missing exception list to add additional exceptions to the detection rule.'
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it dissasociates list from rule when remove exception list clicked ', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<ErrorCallout
|
||||
http={mockKibanaHttpService}
|
||||
errorInfo={{
|
||||
reason: 'not found',
|
||||
code: 404,
|
||||
details: 'list of id "some_uuid" not found',
|
||||
listListId: null,
|
||||
}}
|
||||
rule={{ ...savedRuleMock, exceptions_list: [getListMock()] }}
|
||||
onCancel={jest.fn()}
|
||||
onSuccess={jest.fn()}
|
||||
onError={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').at(0).simulate('click');
|
||||
|
||||
expect(mockDissasociate).toHaveBeenCalledWith([]);
|
||||
});
|
||||
});
|
|
@ -1,169 +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, useEffect, useState, useCallback } from 'react';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiAccordion,
|
||||
EuiCodeBlock,
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { HttpSetup } from '../../../../../../../src/core/public';
|
||||
import { List } from '../../../../common/detection_engine/schemas/types/lists';
|
||||
import { Rule } from '../../../detections/containers/detection_engine/rules/types';
|
||||
import * as i18n from './translations';
|
||||
import { useDissasociateExceptionList } from '../../../detections/containers/detection_engine/rules/use_dissasociate_exception_list';
|
||||
|
||||
export interface ErrorInfo {
|
||||
reason: string | null;
|
||||
code: number | null;
|
||||
details: string | null;
|
||||
listListId: string | null;
|
||||
}
|
||||
|
||||
export interface ErrorCalloutProps {
|
||||
http: HttpSetup;
|
||||
rule: Rule | null;
|
||||
errorInfo: ErrorInfo;
|
||||
onCancel: () => void;
|
||||
onSuccess: (listId: string) => void;
|
||||
onError: (arg: Error) => void;
|
||||
}
|
||||
|
||||
const ErrorCalloutComponent = ({
|
||||
http,
|
||||
rule,
|
||||
errorInfo,
|
||||
onCancel,
|
||||
onError,
|
||||
onSuccess,
|
||||
}: ErrorCalloutProps): JSX.Element => {
|
||||
const [listToDelete, setListToDelete] = useState<List | null>(null);
|
||||
const [errorTitle, setErrorTitle] = useState<string>('');
|
||||
const [errorMessage, setErrorMessage] = useState<string>(i18n.ADD_EXCEPTION_FETCH_ERROR);
|
||||
|
||||
const handleOnSuccess = useCallback((): void => {
|
||||
onSuccess(listToDelete != null ? listToDelete.id : '');
|
||||
}, [onSuccess, listToDelete]);
|
||||
|
||||
const [isDissasociatingList, handleDissasociateExceptionList] = useDissasociateExceptionList({
|
||||
http,
|
||||
ruleRuleId: rule != null ? rule.rule_id : '',
|
||||
onSuccess: handleOnSuccess,
|
||||
onError,
|
||||
});
|
||||
|
||||
const canDisplay404Actions = useMemo(
|
||||
(): boolean =>
|
||||
errorInfo.code === 404 &&
|
||||
rule != null &&
|
||||
listToDelete != null &&
|
||||
handleDissasociateExceptionList != null,
|
||||
[errorInfo.code, listToDelete, handleDissasociateExceptionList, rule]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
// Yes, it's redundant, unfortunately typescript wasn't picking up
|
||||
// that `listToDelete` is checked in canDisplay404Actions
|
||||
if (canDisplay404Actions && listToDelete != null) {
|
||||
setErrorMessage(i18n.ADD_EXCEPTION_FETCH_404_ERROR(listToDelete.id));
|
||||
}
|
||||
|
||||
setErrorTitle(`${errorInfo.reason}${errorInfo.code != null ? ` (${errorInfo.code})` : ''}`);
|
||||
}, [errorInfo.reason, errorInfo.code, listToDelete, canDisplay404Actions]);
|
||||
|
||||
const handleDissasociateList = useCallback((): void => {
|
||||
// Yes, it's redundant, unfortunately typescript wasn't picking up
|
||||
// that `handleDissasociateExceptionList` and `list` are checked in
|
||||
// canDisplay404Actions
|
||||
if (
|
||||
canDisplay404Actions &&
|
||||
rule != null &&
|
||||
listToDelete != null &&
|
||||
handleDissasociateExceptionList != null
|
||||
) {
|
||||
const exceptionLists = (rule.exceptions_list ?? []).filter(
|
||||
({ id }) => id !== listToDelete.id
|
||||
);
|
||||
|
||||
handleDissasociateExceptionList(exceptionLists);
|
||||
}
|
||||
}, [handleDissasociateExceptionList, listToDelete, canDisplay404Actions, rule]);
|
||||
|
||||
useEffect((): void => {
|
||||
if (errorInfo.code === 404 && rule != null && rule.exceptions_list != null) {
|
||||
const [listFound] = rule.exceptions_list.filter(
|
||||
({ id, list_id: listId }) =>
|
||||
(errorInfo.details != null && errorInfo.details.includes(id)) ||
|
||||
errorInfo.listListId === listId
|
||||
);
|
||||
setListToDelete(listFound);
|
||||
}
|
||||
}, [rule, errorInfo.details, errorInfo.code, errorInfo.listListId]);
|
||||
|
||||
return (
|
||||
<EuiCallOut
|
||||
data-test-subj="errorCalloutContainer"
|
||||
title={`${i18n.ERROR}: ${errorTitle}`}
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
>
|
||||
<EuiText size="s">
|
||||
<p data-test-subj="errorCalloutMessage">{errorMessage}</p>
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
{listToDelete != null && (
|
||||
<EuiAccordion
|
||||
id="accordion1"
|
||||
buttonContent={
|
||||
<EuiText size="s">
|
||||
<p>{i18n.MODAL_ERROR_ACCORDION_TEXT}</p>
|
||||
</EuiText>
|
||||
}
|
||||
>
|
||||
<EuiCodeBlock
|
||||
language="json"
|
||||
fontSize="s"
|
||||
paddingSize="m"
|
||||
overflowHeight={300}
|
||||
isCopyable
|
||||
>
|
||||
{JSON.stringify(listToDelete)}
|
||||
</EuiCodeBlock>
|
||||
</EuiAccordion>
|
||||
)}
|
||||
<EuiSpacer />
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="errorCalloutCancelButton"
|
||||
color="danger"
|
||||
isDisabled={isDissasociatingList}
|
||||
onClick={onCancel}
|
||||
>
|
||||
{i18n.CANCEL}
|
||||
</EuiButtonEmpty>
|
||||
{canDisplay404Actions && (
|
||||
<EuiButton
|
||||
data-test-subj="errorCalloutDissasociateButton"
|
||||
isLoading={isDissasociatingList}
|
||||
onClick={handleDissasociateList}
|
||||
color="danger"
|
||||
>
|
||||
{i18n.CLEAR_EXCEPTIONS_LABEL}
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiCallOut>
|
||||
);
|
||||
};
|
||||
|
||||
ErrorCalloutComponent.displayName = 'ErrorCalloutComponent';
|
||||
|
||||
export const ErrorCallout = React.memo(ErrorCalloutComponent);
|
||||
|
||||
ErrorCallout.displayName = 'ErrorCallout';
|
|
@ -190,52 +190,3 @@ export const TOTAL_ITEMS_FETCH_ERROR = i18n.translate(
|
|||
defaultMessage: 'Error getting exception item totals',
|
||||
}
|
||||
);
|
||||
|
||||
export const CLEAR_EXCEPTIONS_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.clearExceptionsLabel',
|
||||
{
|
||||
defaultMessage: 'Remove Exception List',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_EXCEPTION_FETCH_404_ERROR = (listId: string) =>
|
||||
i18n.translate('xpack.securitySolution.exceptions.fetch404Error', {
|
||||
values: { listId },
|
||||
defaultMessage:
|
||||
'The associated exception list ({listId}) no longer exists. Please remove the missing exception list to add additional exceptions to the detection rule.',
|
||||
});
|
||||
|
||||
export const ADD_EXCEPTION_FETCH_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.fetchError',
|
||||
{
|
||||
defaultMessage: 'Error fetching exception list',
|
||||
}
|
||||
);
|
||||
|
||||
export const ERROR = i18n.translate('xpack.securitySolution.exceptions.errorLabel', {
|
||||
defaultMessage: 'Error',
|
||||
});
|
||||
|
||||
export const CANCEL = i18n.translate('xpack.securitySolution.exceptions.cancelLabel', {
|
||||
defaultMessage: 'Cancel',
|
||||
});
|
||||
|
||||
export const MODAL_ERROR_ACCORDION_TEXT = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.modalErrorAccordionText',
|
||||
{
|
||||
defaultMessage: 'Show rule reference information:',
|
||||
}
|
||||
);
|
||||
|
||||
export const DISSASOCIATE_LIST_SUCCESS = (id: string) =>
|
||||
i18n.translate('xpack.securitySolution.exceptions.dissasociateListSuccessText', {
|
||||
values: { id },
|
||||
defaultMessage: 'Exception list ({id}) has successfully been removed',
|
||||
});
|
||||
|
||||
export const DISSASOCIATE_EXCEPTION_LIST_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.dissasociateExceptionListError',
|
||||
{
|
||||
defaultMessage: 'Failed to remove exception list',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -148,50 +148,6 @@ describe('useAddOrUpdateException', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('invokes "onError" if call to add exception item fails', async () => {
|
||||
const mockError = new Error('error adding item');
|
||||
|
||||
addExceptionListItem = jest
|
||||
.spyOn(listsApi, 'addExceptionListItem')
|
||||
.mockRejectedValue(mockError);
|
||||
|
||||
await act(async () => {
|
||||
const { rerender, result, waitForNextUpdate } = render();
|
||||
const addOrUpdateItems = await waitForAddOrUpdateFunc({
|
||||
rerender,
|
||||
result,
|
||||
waitForNextUpdate,
|
||||
});
|
||||
if (addOrUpdateItems) {
|
||||
addOrUpdateItems(...addOrUpdateItemsArgs);
|
||||
}
|
||||
await waitForNextUpdate();
|
||||
expect(onError).toHaveBeenCalledWith(mockError, null, null);
|
||||
});
|
||||
});
|
||||
|
||||
it('invokes "onError" if call to update exception item fails', async () => {
|
||||
const mockError = new Error('error updating item');
|
||||
|
||||
updateExceptionListItem = jest
|
||||
.spyOn(listsApi, 'updateExceptionListItem')
|
||||
.mockRejectedValue(mockError);
|
||||
|
||||
await act(async () => {
|
||||
const { rerender, result, waitForNextUpdate } = render();
|
||||
const addOrUpdateItems = await waitForAddOrUpdateFunc({
|
||||
rerender,
|
||||
result,
|
||||
waitForNextUpdate,
|
||||
});
|
||||
if (addOrUpdateItems) {
|
||||
addOrUpdateItems(...addOrUpdateItemsArgs);
|
||||
}
|
||||
await waitForNextUpdate();
|
||||
expect(onError).toHaveBeenCalledWith(mockError, null, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when alertIdToClose is not passed in', () => {
|
||||
it('should not update the alert status', async () => {
|
||||
await act(async () => {
|
||||
|
|
|
@ -42,7 +42,7 @@ export type ReturnUseAddOrUpdateException = [
|
|||
|
||||
export interface UseAddOrUpdateExceptionProps {
|
||||
http: HttpStart;
|
||||
onError: (arg: Error, code: number | null, message: string | null) => void;
|
||||
onError: (arg: Error) => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
|
@ -157,11 +157,7 @@ export const useAddOrUpdateException = ({
|
|||
} catch (error) {
|
||||
if (isSubscribed) {
|
||||
setIsLoading(false);
|
||||
if (error.body != null) {
|
||||
onError(error, error.body.status_code, error.body.message);
|
||||
} else {
|
||||
onError(error, null, null);
|
||||
}
|
||||
onError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -379,7 +379,7 @@ describe('useFetchOrCreateRuleExceptionList', () => {
|
|||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
expect(onError).toHaveBeenCalledWith(error, null, null);
|
||||
expect(onError).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ export interface UseFetchOrCreateRuleExceptionListProps {
|
|||
http: HttpStart;
|
||||
ruleId: Rule['id'];
|
||||
exceptionListType: ExceptionListSchema['type'];
|
||||
onError: (arg: Error, code: number | null, message: string | null) => void;
|
||||
onError: (arg: Error) => void;
|
||||
onSuccess?: (ruleWasChanged: boolean) => void;
|
||||
}
|
||||
|
||||
|
@ -179,11 +179,7 @@ export const useFetchOrCreateRuleExceptionList = ({
|
|||
if (isSubscribed) {
|
||||
setIsLoading(false);
|
||||
setExceptionList(null);
|
||||
if (error.body != null) {
|
||||
onError(error, error.body.status_code, error.body.message);
|
||||
} else {
|
||||
onError(error, null, null);
|
||||
}
|
||||
onError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -322,13 +322,11 @@ const ExceptionsViewerComponent = ({
|
|||
exceptionListTypeToEdit != null && (
|
||||
<EditExceptionModal
|
||||
ruleName={ruleName}
|
||||
ruleId={ruleId}
|
||||
ruleIndices={ruleIndices}
|
||||
exceptionListType={exceptionListTypeToEdit}
|
||||
exceptionItem={exceptionToEdit}
|
||||
onCancel={handleOnCancelExceptionModal}
|
||||
onConfirm={handleOnConfirmExceptionModal}
|
||||
onRuleChange={onRuleChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
@ -1,52 +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 { act, renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { createKibanaCoreStartMock } from '../../../../common/mock/kibana_core';
|
||||
|
||||
import * as api from './api';
|
||||
import { ruleMock } from './mock';
|
||||
import {
|
||||
ReturnUseDissasociateExceptionList,
|
||||
UseDissasociateExceptionListProps,
|
||||
useDissasociateExceptionList,
|
||||
} from './use_dissasociate_exception_list';
|
||||
|
||||
const mockKibanaHttpService = createKibanaCoreStartMock().http;
|
||||
|
||||
describe('useDissasociateExceptionList', () => {
|
||||
const onError = jest.fn();
|
||||
const onSuccess = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(api, 'patchRule').mockResolvedValue(ruleMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('initializes hook', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<
|
||||
UseDissasociateExceptionListProps,
|
||||
ReturnUseDissasociateExceptionList
|
||||
>(() =>
|
||||
useDissasociateExceptionList({
|
||||
http: mockKibanaHttpService,
|
||||
ruleRuleId: 'rule_id',
|
||||
onError,
|
||||
onSuccess,
|
||||
})
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual([false, 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 { useEffect, useState, useRef } from 'react';
|
||||
|
||||
import { HttpStart } from '../../../../../../../../src/core/public';
|
||||
import { List } from '../../../../../common/detection_engine/schemas/types/lists';
|
||||
import { patchRule } from './api';
|
||||
|
||||
type Func = (lists: List[]) => void;
|
||||
export type ReturnUseDissasociateExceptionList = [boolean, Func | null];
|
||||
|
||||
export interface UseDissasociateExceptionListProps {
|
||||
http: HttpStart;
|
||||
ruleRuleId: string;
|
||||
onError: (arg: Error) => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for removing an exception list reference from a rule
|
||||
*
|
||||
* @param http Kibana http service
|
||||
* @param ruleRuleId a rule_id (NOT id)
|
||||
* @param onError error callback
|
||||
* @param onSuccess success callback
|
||||
*
|
||||
*/
|
||||
export const useDissasociateExceptionList = ({
|
||||
http,
|
||||
ruleRuleId,
|
||||
onError,
|
||||
onSuccess,
|
||||
}: UseDissasociateExceptionListProps): ReturnUseDissasociateExceptionList => {
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const dissasociateList = useRef<Func | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
const dissasociateListFromRule = (id: string) => async (
|
||||
exceptionLists: List[]
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (isSubscribed) {
|
||||
setLoading(true);
|
||||
|
||||
await patchRule({
|
||||
ruleProperties: {
|
||||
rule_id: id,
|
||||
exceptions_list: exceptionLists,
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
onSuccess();
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
if (isSubscribed) {
|
||||
setLoading(false);
|
||||
onError(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
dissasociateList.current = dissasociateListFromRule(ruleRuleId);
|
||||
|
||||
return (): void => {
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, [http, ruleRuleId, onError, onSuccess]);
|
||||
|
||||
return [isLoading, dissasociateList.current];
|
||||
};
|
Loading…
Reference in a new issue