[SecuritySolution] Add success toast to timeline deletion (#99612)

* Add success toast to timeline deletion

* Add unit tests for timeline deletion toast

* Refactor export_timeline to use useAppToasts instead of useStateToaster
This commit is contained in:
Pablo Machado 2021-05-10 16:58:50 +02:00 committed by GitHub
parent 5f618da802
commit 0ffe4c7a54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 87 additions and 44 deletions

View file

@ -11,6 +11,10 @@ import { useParams } from 'react-router-dom';
import { DeleteTimelineModalOverlay } from '.';
import { TimelineType } from '../../../../../common/types/timeline';
import * as i18n from '../translations';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
jest.mock('../../../../common/hooks/use_app_toasts');
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
@ -21,12 +25,19 @@ jest.mock('react-router-dom', () => {
});
describe('DeleteTimelineModal', () => {
const savedObjectId = 'abcd';
const mockAddSuccess = jest.fn();
(useAppToasts as jest.Mock).mockReturnValue({ addSuccess: mockAddSuccess });
afterEach(() => {
mockAddSuccess.mockClear();
});
const savedObjectIds = ['abcd'];
const defaultProps = {
closeModal: jest.fn(),
deleteTimelines: jest.fn(),
isModalOpen: true,
savedObjectIds: [savedObjectId],
savedObjectIds,
title: 'Privilege Escalation',
};
@ -56,5 +67,25 @@ describe('DeleteTimelineModal', () => {
expect(wrapper.find('[data-test-subj="remove-popover"]').first().exists()).toBe(true);
});
test('it shows correct toast message on success for deleted timelines', async () => {
const wrapper = mountWithIntl(<DeleteTimelineModalOverlay {...defaultProps} />);
wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click');
expect(mockAddSuccess.mock.calls[0][0].title).toEqual(
i18n.SUCCESSFULLY_DELETED_TIMELINES(savedObjectIds.length)
);
});
test('it shows correct toast message on success for deleted templates', async () => {
(useParams as jest.Mock).mockReturnValue({ tabName: TimelineType.template });
const wrapper = mountWithIntl(<DeleteTimelineModalOverlay {...defaultProps} />);
wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click');
expect(mockAddSuccess.mock.calls[0][0].title).toEqual(
i18n.SUCCESSFULLY_DELETED_TIMELINE_TEMPLATES(savedObjectIds.length)
);
});
});
});

View file

@ -9,8 +9,13 @@ import { EuiModal } from '@elastic/eui';
import React, { useCallback } from 'react';
import { createGlobalStyle } from 'styled-components';
import { useParams } from 'react-router-dom';
import { DeleteTimelineModal, DELETE_TIMELINE_MODAL_WIDTH } from './delete_timeline_modal';
import { DeleteTimelines } from '../types';
import { TimelineType } from '../../../../../common/types/timeline';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import * as i18n from '../translations';
const RemovePopover = createGlobalStyle`
div.euiPopover__panel-isOpen {
display: none;
@ -29,19 +34,29 @@ interface Props {
*/
export const DeleteTimelineModalOverlay = React.memo<Props>(
({ deleteTimelines, isModalOpen, savedObjectIds, title, onComplete }) => {
const { addSuccess } = useAppToasts();
const { tabName: timelineType } = useParams<{ tabName: TimelineType }>();
const internalCloseModal = useCallback(() => {
if (onComplete != null) {
onComplete();
}
}, [onComplete]);
const onDelete = useCallback(() => {
if (savedObjectIds != null) {
if (savedObjectIds.length > 0) {
deleteTimelines(savedObjectIds);
addSuccess({
title:
timelineType === TimelineType.template
? i18n.SUCCESSFULLY_DELETED_TIMELINE_TEMPLATES(savedObjectIds.length)
: i18n.SUCCESSFULLY_DELETED_TIMELINES(savedObjectIds.length),
});
}
if (onComplete != null) {
onComplete();
}
}, [deleteTimelines, savedObjectIds, onComplete]);
}, [deleteTimelines, savedObjectIds, onComplete, addSuccess, timelineType]);
return (
<>
{isModalOpen && <RemovePopover data-test-subj="remove-popover" />}

View file

@ -6,7 +6,6 @@
*/
import React from 'react';
import { useStateToaster } from '../../../../common/components/toasters';
import { TimelineDownloader } from './export_timeline';
import { mockSelectedTimeline } from './mocks';
@ -16,12 +15,9 @@ import { ReactWrapper, mount } from 'enzyme';
import { waitFor } from '@testing-library/react';
import { useParams } from 'react-router-dom';
jest.mock('../translations', () => {
return {
EXPORT_SELECTED: 'EXPORT_SELECTED',
EXPORT_FILENAME: 'TIMELINE',
};
});
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
jest.mock('../../../../common/hooks/use_app_toasts');
jest.mock('.', () => {
return {
@ -38,34 +34,26 @@ jest.mock('react-router-dom', () => {
};
});
jest.mock('../../../../common/components/toasters', () => {
const actual = jest.requireActual('../../../../common/components/toasters');
return {
...actual,
useStateToaster: jest.fn(),
};
});
describe('TimelineDownloader', () => {
const mockAddSuccess = jest.fn();
(useAppToasts as jest.Mock).mockReturnValue({ addSuccess: mockAddSuccess });
let wrapper: ReactWrapper;
const exportedIds = ['baa20980-6301-11ea-9223-95b6d4dd806c'];
const defaultTestProps = {
exportedIds: ['baa20980-6301-11ea-9223-95b6d4dd806c'],
exportedIds,
getExportedData: jest.fn(),
isEnableDownloader: true,
onComplete: jest.fn(),
};
const mockDispatchToaster = jest.fn();
beforeEach(() => {
(useStateToaster as jest.Mock).mockReturnValue([jest.fn(), mockDispatchToaster]);
(useParams as jest.Mock).mockReturnValue({ tabName: 'default' });
});
afterEach(() => {
(useStateToaster as jest.Mock).mockClear();
(useParams as jest.Mock).mockReset();
(mockDispatchToaster as jest.Mock).mockClear();
mockAddSuccess.mockClear();
});
describe('should not render a downloader', () => {
@ -104,11 +92,12 @@ describe('TimelineDownloader', () => {
};
wrapper = mount(<TimelineDownloader {...testProps} />);
await waitFor(() => {
wrapper.update();
expect(mockDispatchToaster.mock.calls[0][0].title).toEqual(
i18n.SUCCESSFULLY_EXPORTED_TIMELINES
expect(mockAddSuccess.mock.calls[0][0].title).toEqual(
i18n.SUCCESSFULLY_EXPORTED_TIMELINES(exportedIds.length)
);
});
});
@ -124,8 +113,8 @@ describe('TimelineDownloader', () => {
await waitFor(() => {
wrapper.update();
expect(mockDispatchToaster.mock.calls[0][0].title).toEqual(
i18n.SUCCESSFULLY_EXPORTED_TIMELINES
expect(mockAddSuccess.mock.calls[0][0].title).toEqual(
i18n.SUCCESSFULLY_EXPORTED_TIMELINE_TEMPLATES(exportedIds.length)
);
});
});

View file

@ -6,7 +6,6 @@
*/
import React, { useCallback } from 'react';
import uuid from 'uuid';
import { useParams } from 'react-router-dom';
import {
@ -14,8 +13,8 @@ import {
ExportSelectedData,
} from '../../../../common/components/generic_downloader';
import * as i18n from '../translations';
import { useStateToaster } from '../../../../common/components/toasters';
import { TimelineType } from '../../../../../common/types/timeline';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
const ExportTimeline: React.FC<{
exportedIds: string[] | undefined;
@ -23,8 +22,8 @@ const ExportTimeline: React.FC<{
isEnableDownloader: boolean;
onComplete?: () => void;
}> = ({ onComplete, isEnableDownloader, exportedIds, getExportedData }) => {
const [, dispatchToaster] = useStateToaster();
const { tabName: timelineType } = useParams<{ tabName: TimelineType }>();
const { addSuccess } = useAppToasts();
const onExportSuccess = useCallback(
(exportCount) => {
@ -32,20 +31,15 @@ const ExportTimeline: React.FC<{
onComplete();
}
dispatchToaster({
type: 'addToaster',
toast: {
id: uuid.v4(),
title:
timelineType === TimelineType.template
? i18n.SUCCESSFULLY_EXPORTED_TIMELINE_TEMPLATES(exportCount)
: i18n.SUCCESSFULLY_EXPORTED_TIMELINES(exportCount),
color: 'success',
iconType: 'check',
},
addSuccess({
title:
timelineType === TimelineType.template
? i18n.SUCCESSFULLY_EXPORTED_TIMELINE_TEMPLATES(exportCount)
: i18n.SUCCESSFULLY_EXPORTED_TIMELINES(exportCount),
'data-test-subj': 'addObjectToContainerSuccess',
});
},
[dispatchToaster, onComplete, timelineType]
[addSuccess, onComplete, timelineType]
);
const onExportFailure = useCallback(() => {
if (onComplete != null) {

View file

@ -293,6 +293,20 @@ export const SUCCESSFULLY_EXPORTED_TIMELINE_TEMPLATES = (totalTimelineTemplates:
}
);
export const SUCCESSFULLY_DELETED_TIMELINES = (totalTimelines: number) =>
i18n.translate('xpack.securitySolution.open.timeline.successfullyDeletedTimelinesTitle', {
values: { totalTimelines },
defaultMessage:
'Successfully deleted {totalTimelines, plural, =0 {all timelines} =1 {{totalTimelines} timeline} other {{totalTimelines} timelines}}',
});
export const SUCCESSFULLY_DELETED_TIMELINE_TEMPLATES = (totalTimelineTemplates: number) =>
i18n.translate('xpack.securitySolution.open.timeline.successfullyDeletedTimelineTemplatesTitle', {
values: { totalTimelineTemplates },
defaultMessage:
'Successfully deleted {totalTimelineTemplates, plural, =0 {all timelines} =1 {{totalTimelineTemplates} timeline template} other {{totalTimelineTemplates} timeline templates}}',
});
export const TAB_TIMELINES = i18n.translate(
'xpack.securitySolution.timelines.components.tabs.timelinesTitle',
{