diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx index f70f96a26c83a..9d0f68a9bb483 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx @@ -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 = ({ 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 ( diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/translations.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/translations.ts index a32f9c263be49..2c3f5e30b5b5d 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/translations.ts @@ -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', + } +); diff --git a/x-pack/plugins/security_solution/public/common/utils/timeline/use_timeline_click.tsx b/x-pack/plugins/security_solution/public/common/utils/timeline/use_timeline_click.tsx index 2756ba2a696e1..826ac7c32b7b0 100644 --- a/x-pack/plugins/security_solution/public/common/utils/timeline/use_timeline_click.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/timeline/use_timeline_click.tsx @@ -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, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 69b63e83186e3..5d52d2c8a4d48 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -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, @@ -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)), diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index c72aa5878478d..2a3b49517b456 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -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'; @@ -313,6 +318,7 @@ export interface QueryTimelineById { graphEventId?: string; timelineId: string; timelineType?: TimelineType; + onError?: TimelineErrorCallback; onOpenTimeline?: (timeline: TimelineModel) => void; openTimeline?: boolean; updateIsLoading: ({ @@ -331,6 +337,7 @@ export const queryTimelineById = ({ graphEventId = '', timelineId, timelineType, + onError, onOpenTimeline, openTimeline = true, updateIsLoading, @@ -372,6 +379,11 @@ export const queryTimelineById = ({ })(); } }) + .catch((error) => { + if (onError != null) { + onError(error, timelineId); + } + }) .finally(() => { updateIsLoading({ id: TimelineId.active, isLoading: false }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index cddf4e8d71d60..79a700856c00f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -235,3 +235,5 @@ export interface TemplateTimelineFilter { withNext: boolean; count: number | undefined; } + +export type TimelineErrorCallback = (error: Error, timelineId: string) => void;