Skip to content

Commit

Permalink
[SecuritySolution] Add success toast to timeline deletion (#99612) (#…
Browse files Browse the repository at this point in the history
…99764)

* Add success toast to timeline deletion

* Add unit tests for timeline deletion toast

* Refactor export_timeline to use useAppToasts instead of useStateToaster
  • Loading branch information
machadoum authored May 11, 2021
1 parent f3c66e8 commit 1b501aa
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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',
};

Expand Down Expand Up @@ -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)
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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" />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
*/

import React from 'react';
import { useStateToaster } from '../../../../common/components/toasters';

import { TimelineDownloader } from './export_timeline';
import { mockSelectedTimeline } from './mocks';
Expand All @@ -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 {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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)
);
});
});
Expand All @@ -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)
);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,46 +6,40 @@
*/

import React, { useCallback } from 'react';
import uuid from 'uuid';
import { useParams } from 'react-router-dom';

import {
GenericDownloader,
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;
getExportedData: ExportSelectedData;
isEnableDownloader: boolean;
onComplete?: () => void;
}> = ({ onComplete, isEnableDownloader, exportedIds, getExportedData }) => {
const [, dispatchToaster] = useStateToaster();
const { tabName: timelineType } = useParams<{ tabName: TimelineType }>();
const { addSuccess } = useAppToasts();

const onExportSuccess = useCallback(
(exportCount) => {
if (onComplete != null) {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
{
Expand Down

0 comments on commit 1b501aa

Please sign in to comment.