Skip to content

Commit

Permalink
[Cases][Timeline] Toast an error when unable to retrieve timeline (el…
Browse files Browse the repository at this point in the history
…astic#113795)

* Displaying a toaster error message when a timeline fails to load

* Adding tests
  • Loading branch information
jonathan-buttner authored Oct 5, 2021
1 parent 940149c commit 93d1a7f
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,30 @@ import { EuiToolTip, EuiLink } from '@elastic/eui';
import { useTimelineClick } from '../../../../utils/timeline/use_timeline_click';
import { TimelineProps } from './types';
import * as i18n from './translations';
import { useAppToasts } from '../../../../hooks/use_app_toasts';

export const TimelineMarkDownRendererComponent: React.FC<TimelineProps> = ({
id,
title,
graphEventId,
}) => {
const { addError } = useAppToasts();

const handleTimelineClick = useTimelineClick();

const onError = useCallback(
(error: Error, timelineId: string) => {
addError(error, {
title: i18n.TIMELINE_ERROR_TITLE,
toastMessage: i18n.FAILED_TO_RETRIEVE_TIMELINE(timelineId),
});
},
[addError]
);

const onClickTimeline = useCallback(
() => handleTimelineClick(id ?? '', graphEventId),
[id, graphEventId, handleTimelineClick]
() => handleTimelineClick(id ?? '', onError, graphEventId),
[id, graphEventId, handleTimelineClick, onError]
);
return (
<EuiToolTip content={i18n.TIMELINE_ID(id ?? '')}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,19 @@ export const NO_PARENTHESES = i18n.translate(
defaultMessage: 'Expected left parentheses',
}
);

export const FAILED_TO_RETRIEVE_TIMELINE = (timelineId: string) =>
i18n.translate(
'xpack.securitySolution.markdownEditor.plugins.timeline.failedRetrieveTimelineErrorMsg',
{
defaultMessage: 'Failed to retrieve timeline id: { timelineId }',
values: { timelineId },
}
);

export const TIMELINE_ERROR_TITLE = i18n.translate(
'xpack.securitySolution.markdownEditor.plugins.timeline.timelineErrorTitle',
{
defaultMessage: 'Timeline Error',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,18 @@ import {
dispatchUpdateTimeline,
queryTimelineById,
} from '../../../timelines/components/open_timeline/helpers';
import { TimelineErrorCallback } from '../../../timelines/components/open_timeline/types';
import { updateIsLoading as dispatchUpdateIsLoading } from '../../../timelines/store/timeline/actions';

export const useTimelineClick = () => {
const dispatch = useDispatch();

const handleTimelineClick = useCallback(
(timelineId: string, graphEventId?: string) => {
(timelineId: string, onError: TimelineErrorCallback, graphEventId?: string) => {
queryTimelineById({
graphEventId,
timelineId,
onError,
updateIsLoading: ({
id: currentTimelineId,
isLoading,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -939,17 +939,46 @@ describe('helpers', () => {
});

describe('queryTimelineById', () => {
describe('encounters failure when retrieving a timeline', () => {
const onError = jest.fn();
const mockError = new Error('failed');

const args = {
timelineId: '123',
onError,
updateIsLoading: jest.fn(),
updateTimeline: jest.fn(),
};

beforeAll(async () => {
(getTimeline as jest.Mock).mockRejectedValue(mockError);
queryTimelineById<{}>(args as unknown as QueryTimelineById<{}>);
});

afterAll(() => {
jest.clearAllMocks();
});

test('calls onError with the error', () => {
expect(onError).toHaveBeenCalledWith(mockError, '123');
});
});

describe('open a timeline', () => {
const updateIsLoading = jest.fn();
const selectedTimeline = {
...mockSelectedTimeline,
};

const updateIsLoading = jest.fn();
const onOpenTimeline = jest.fn();
const onError = jest.fn();

const args = {
duplicate: false,
graphEventId: '',
timelineId: '',
timelineType: TimelineType.default,
onError,
onOpenTimeline,
openTimeline: true,
updateIsLoading,
Expand All @@ -976,6 +1005,10 @@ describe('helpers', () => {
expect(getTimeline).toHaveBeenCalled();
});

test('it does not call onError when an error does not occur', () => {
expect(onError).not.toHaveBeenCalled();
});

test('Do not override daterange if TimelineStatus is active', () => {
const { timeline } = formatTimelineResultToModel(
omitTypenameInTimeline(getOr({}, 'data.getOneTimeline', selectedTimeline)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ import {
DEFAULT_COLUMN_MIN_WIDTH,
} from '../timeline/body/constants';

import { OpenTimelineResult, UpdateTimeline, DispatchUpdateTimeline } from './types';
import {
OpenTimelineResult,
UpdateTimeline,
DispatchUpdateTimeline,
TimelineErrorCallback,
} from './types';
import { createNote } from '../notes/helpers';
import { IS_OPERATOR } from '../timeline/data_providers/data_provider';
import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range';
Expand Down Expand Up @@ -313,6 +318,7 @@ export interface QueryTimelineById<TCache> {
graphEventId?: string;
timelineId: string;
timelineType?: TimelineType;
onError?: TimelineErrorCallback;
onOpenTimeline?: (timeline: TimelineModel) => void;
openTimeline?: boolean;
updateIsLoading: ({
Expand All @@ -331,6 +337,7 @@ export const queryTimelineById = <TCache>({
graphEventId = '',
timelineId,
timelineType,
onError,
onOpenTimeline,
openTimeline = true,
updateIsLoading,
Expand Down Expand Up @@ -372,6 +379,11 @@ export const queryTimelineById = <TCache>({
})();
}
})
.catch((error) => {
if (onError != null) {
onError(error, timelineId);
}
})
.finally(() => {
updateIsLoading({ id: TimelineId.active, isLoading: false });
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,5 @@ export interface TemplateTimelineFilter {
withNext: boolean;
count: number | undefined;
}

export type TimelineErrorCallback = (error: Error, timelineId: string) => void;

0 comments on commit 93d1a7f

Please sign in to comment.