Revert "[Security Solution][Exceptions] - Improve UX for missing exception list associated with rule (#75898)"

This reverts commit b9c8201202.
This commit is contained in:
Tyler Smalley 2020-08-26 08:41:09 -07:00
parent d6c45a2e70
commit e773f221a3
13 changed files with 54 additions and 706 deletions

View file

@ -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>
);

View file

@ -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()}

View file

@ -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>
);

View file

@ -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([]);
});
});

View file

@ -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';

View file

@ -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',
}
);

View file

@ -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 () => {

View file

@ -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);
}
}
};

View file

@ -379,7 +379,7 @@ describe('useFetchOrCreateRuleExceptionList', () => {
await waitForNextUpdate();
await waitForNextUpdate();
expect(onError).toHaveBeenCalledTimes(1);
expect(onError).toHaveBeenCalledWith(error, null, null);
expect(onError).toHaveBeenCalledWith(error);
});
});

View file

@ -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);
}
}
}

View file

@ -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}
/>
)}

View file

@ -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]);
});
});
});

View file

@ -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];
};