[Security Solution][Case] Add button to go to case view after adding an alert to a case (#89214)
This commit is contained in:
parent
7bb8d3a7b2
commit
0c2c451830
|
@ -6,18 +6,24 @@
|
|||
|
||||
/* eslint-disable react/display-name */
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
import { mount } from 'enzyme';
|
||||
import { EuiGlobalToastList } from '@elastic/eui';
|
||||
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { useStateToaster } from '../../../common/components/toasters';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { usePostComment } from '../../containers/use_post_comment';
|
||||
import { Case } from '../../containers/types';
|
||||
import { AddToCaseAction } from './add_to_case_action';
|
||||
|
||||
jest.mock('../../containers/use_post_comment');
|
||||
jest.mock('../../../common/lib/kibana', () => {
|
||||
const originalModule = jest.requireActual('../../../common/lib/kibana');
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
jest.mock('../../../common/components/toasters', () => {
|
||||
const actual = jest.requireActual('../../../common/components/toasters');
|
||||
return {
|
||||
...originalModule,
|
||||
useGetUserSavedObjectPermissions: jest.fn(),
|
||||
...actual,
|
||||
useStateToaster: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -44,14 +50,16 @@ jest.mock('../create/form_context', () => {
|
|||
onSuccess,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
onSuccess: ({ id }: { id: string }) => void;
|
||||
onSuccess: (theCase: Partial<Case>) => void;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="form-context-on-success"
|
||||
onClick={() => onSuccess({ id: 'new-case' })}
|
||||
onClick={() =>
|
||||
onSuccess({ id: 'new-case', title: 'the new case', settings: { syncAlerts: true } })
|
||||
}
|
||||
>
|
||||
{'submit'}
|
||||
</button>
|
||||
|
@ -95,9 +103,16 @@ describe('AddToCaseAction', () => {
|
|||
disabled: false,
|
||||
};
|
||||
|
||||
const mockDispatchToaster = jest.fn();
|
||||
const mockNavigateToApp = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
usePostCommentMock.mockImplementation(() => defaultPostComment);
|
||||
(useStateToaster as jest.Mock).mockReturnValue([jest.fn(), mockDispatchToaster]);
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: { application: { navigateToApp: mockNavigateToApp } },
|
||||
});
|
||||
});
|
||||
|
||||
it('it renders', async () => {
|
||||
|
@ -187,4 +202,37 @@ describe('AddToCaseAction', () => {
|
|||
type: 'alert',
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates to case view', async () => {
|
||||
usePostCommentMock.mockImplementation(() => {
|
||||
return {
|
||||
...defaultPostComment,
|
||||
postComment: jest.fn().mockImplementation((caseId, data, updateCase) => updateCase()),
|
||||
};
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AddToCaseAction {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click');
|
||||
wrapper.find(`[data-test-subj="add-new-case-item"]`).first().simulate('click');
|
||||
wrapper.find(`[data-test-subj="form-context-on-success"]`).first().simulate('click');
|
||||
|
||||
expect(mockDispatchToaster).toHaveBeenCalled();
|
||||
const toast = mockDispatchToaster.mock.calls[0][0].toast;
|
||||
|
||||
const toastWrapper = mount(
|
||||
<EuiGlobalToastList toasts={[toast]} toastLifeTimeMs={6000} dismissToast={() => {}} />
|
||||
);
|
||||
|
||||
toastWrapper
|
||||
.find('[data-test-subj="toaster-content-case-view-link"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
expect(mockNavigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/new-case' });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,6 +20,10 @@ import { ActionIconItem } from '../../../timelines/components/timeline/body/acti
|
|||
import { usePostComment } from '../../containers/use_post_comment';
|
||||
import { Case } from '../../containers/types';
|
||||
import { useStateToaster } from '../../../common/components/toasters';
|
||||
import { APP_ID } from '../../../../common/constants';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { getCaseDetailsUrl } from '../../../common/components/link_to';
|
||||
import { SecurityPageName } from '../../../app/types';
|
||||
import { useCreateCaseModal } from '../use_create_case_modal';
|
||||
import { useAllCasesModal } from '../use_all_cases_modal';
|
||||
import { createUpdateSuccessToaster } from './helpers';
|
||||
|
@ -39,12 +43,23 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
|
|||
const eventId = ecsRowData._id;
|
||||
const eventIndex = ecsRowData._index;
|
||||
|
||||
const { navigateToApp } = useKibana().services.application;
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const openPopover = useCallback(() => setIsPopoverOpen(true), []);
|
||||
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
|
||||
|
||||
const { postComment } = usePostComment();
|
||||
|
||||
const onViewCaseClick = useCallback(
|
||||
(id) => {
|
||||
navigateToApp(`${APP_ID}:${SecurityPageName.case}`, {
|
||||
path: getCaseDetailsUrl({ id }),
|
||||
});
|
||||
},
|
||||
[navigateToApp]
|
||||
);
|
||||
|
||||
const attachAlertToCase = useCallback(
|
||||
(theCase: Case) => {
|
||||
postComment(
|
||||
|
@ -54,10 +69,14 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
|
|||
alertId: eventId,
|
||||
index: eventIndex ?? '',
|
||||
},
|
||||
() => dispatchToaster({ type: 'addToaster', toast: createUpdateSuccessToaster(theCase) })
|
||||
() =>
|
||||
dispatchToaster({
|
||||
type: 'addToaster',
|
||||
toast: createUpdateSuccessToaster(theCase, onViewCaseClick),
|
||||
})
|
||||
);
|
||||
},
|
||||
[postComment, eventId, eventIndex, dispatchToaster]
|
||||
[postComment, eventId, eventIndex, dispatchToaster, onViewCaseClick]
|
||||
);
|
||||
|
||||
const { modal: createCaseModal, openModal: openCreateCaseModal } = useCreateCaseModal({
|
||||
|
|
|
@ -8,6 +8,7 @@ import { createUpdateSuccessToaster } from './helpers';
|
|||
import { Case } from '../../containers/types';
|
||||
|
||||
const theCase = {
|
||||
id: 'case-id',
|
||||
title: 'My case',
|
||||
settings: {
|
||||
syncAlerts: true,
|
||||
|
@ -15,24 +16,13 @@ const theCase = {
|
|||
} as Case;
|
||||
|
||||
describe('helpers', () => {
|
||||
const onViewCaseClick = jest.fn();
|
||||
|
||||
describe('createUpdateSuccessToaster', () => {
|
||||
it('creates the correct toast when the sync alerts is on', () => {
|
||||
// We remove the id as is randomly generated
|
||||
const { id, ...toast } = createUpdateSuccessToaster(theCase);
|
||||
expect(toast).toEqual({
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
text: 'Alerts in this case have their status synched with the case status',
|
||||
title: 'An alert has been added to "My case"',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates the correct toast when the sync alerts is off', () => {
|
||||
// We remove the id as is randomly generated
|
||||
const { id, ...toast } = createUpdateSuccessToaster({
|
||||
...theCase,
|
||||
settings: { syncAlerts: false },
|
||||
});
|
||||
// We remove the id as is randomly generated and the text as it is a React component
|
||||
// which is being test on toaster_content.test.tsx
|
||||
const { id, text, ...toast } = createUpdateSuccessToaster(theCase, onViewCaseClick);
|
||||
expect(toast).toEqual({
|
||||
color: 'success',
|
||||
iconType: 'check',
|
|
@ -4,22 +4,28 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import uuid from 'uuid';
|
||||
import { AppToast } from '../../../common/components/toasters';
|
||||
import { Case } from '../../containers/types';
|
||||
import { ToasterContent } from './toaster_content';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const createUpdateSuccessToaster = (theCase: Case): AppToast => {
|
||||
const toast: AppToast = {
|
||||
export const createUpdateSuccessToaster = (
|
||||
theCase: Case,
|
||||
onViewCaseClick: (id: string) => void
|
||||
): AppToast => {
|
||||
return {
|
||||
id: uuid.v4(),
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
title: i18n.CASE_CREATED_SUCCESS_TOAST(theCase.title),
|
||||
text: (
|
||||
<ToasterContent
|
||||
caseId={theCase.id}
|
||||
syncAlerts={theCase.settings.syncAlerts}
|
||||
onViewCaseClick={onViewCaseClick}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
if (theCase.settings.syncAlerts) {
|
||||
return { ...toast, text: i18n.CASE_CREATED_SUCCESS_TOAST_TEXT };
|
||||
}
|
||||
|
||||
return toast;
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 { mount } from 'enzyme';
|
||||
|
||||
import { ToasterContent } from './toaster_content';
|
||||
|
||||
describe('ToasterContent', () => {
|
||||
const onViewCaseClick = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders with syncAlerts=true', () => {
|
||||
const wrapper = mount(
|
||||
<ToasterContent caseId="case-id" syncAlerts={true} onViewCaseClick={onViewCaseClick} />
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="toaster-content-case-view-link"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="toaster-content-sync-text"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders with syncAlerts=false', () => {
|
||||
const wrapper = mount(
|
||||
<ToasterContent caseId="case-id" syncAlerts={false} onViewCaseClick={onViewCaseClick} />
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="toaster-content-case-view-link"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="toaster-content-sync-text"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('calls onViewCaseClick', () => {
|
||||
const wrapper = mount(
|
||||
<ToasterContent caseId="case-id" syncAlerts={false} onViewCaseClick={onViewCaseClick} />
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="toaster-content-case-view-link"]').first().simulate('click');
|
||||
expect(onViewCaseClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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, { memo, useCallback } from 'react';
|
||||
import { EuiButtonEmpty, EuiText } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
const EuiTextStyled = styled(EuiText)`
|
||||
${({ theme }) => `
|
||||
margin-bottom: ${theme.eui?.paddingSizes?.s ?? 8}px;
|
||||
`}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
caseId: string;
|
||||
syncAlerts: boolean;
|
||||
onViewCaseClick: (id: string) => void;
|
||||
}
|
||||
|
||||
const ToasterContentComponent: React.FC<Props> = ({ caseId, syncAlerts, onViewCaseClick }) => {
|
||||
const onClick = useCallback(() => onViewCaseClick(caseId), [caseId, onViewCaseClick]);
|
||||
return (
|
||||
<>
|
||||
{syncAlerts && (
|
||||
<EuiTextStyled size="s" data-test-subj="toaster-content-sync-text">
|
||||
{i18n.CASE_CREATED_SUCCESS_TOAST_TEXT}
|
||||
</EuiTextStyled>
|
||||
)}
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
flush="left"
|
||||
onClick={onClick}
|
||||
data-test-subj="toaster-content-case-view-link"
|
||||
>
|
||||
{i18n.VIEW_CASE}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ToasterContent = memo(ToasterContentComponent);
|
|
@ -53,3 +53,10 @@ export const CASE_CREATED_SUCCESS_TOAST_TEXT = i18n.translate(
|
|||
defaultMessage: 'Alerts in this case have their status synched with the case status',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_CASE = i18n.translate(
|
||||
'xpack.securitySolution.case.timeline.actions.caseCreatedSuccessToastViewCaseLink',
|
||||
{
|
||||
defaultMessage: 'View Case',
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue