Skip to content

Commit 75582eb

Browse files
[SECURITY] Timeline bug 7.9 (#71748)
* remove delay of rendering row * Fix flyout timeline to behave as we wanted * Fix tabs on timeline page * disable sensor visibility when you have less than 100 events in timeline * Fix container to fit content and not take all the place that it wants * do not update timeline time when switching top nav * fix timeline url in case * review I Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent 667b72f commit 75582eb

File tree

27 files changed

+245
-200
lines changed

27 files changed

+245
-200
lines changed

x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx

Lines changed: 6 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { EuiButton, EuiLoadingSpinner } from '@elastic/eui';
88
import React, { useCallback, useEffect } from 'react';
99
import styled from 'styled-components';
1010

11-
import { useDispatch } from 'react-redux';
1211
import { CommentRequest } from '../../../../../case/common/api';
1312
import { usePostComment } from '../../containers/use_post_comment';
1413
import { Case } from '../../containers/types';
@@ -19,12 +18,7 @@ import { Form, useForm, UseField } from '../../../shared_imports';
1918

2019
import * as i18n from './translations';
2120
import { schema } from './schema';
22-
import {
23-
dispatchUpdateTimeline,
24-
queryTimelineById,
25-
} from '../../../timelines/components/open_timeline/helpers';
26-
import { updateIsLoading as dispatchUpdateIsLoading } from '../../../timelines/store/timeline/actions';
27-
import { useApolloClient } from '../../../common/utils/apollo_context';
21+
import { useTimelineClick } from '../utils/use_timeline_click';
2822

2923
const MySpinner = styled(EuiLoadingSpinner)`
3024
position: absolute;
@@ -53,8 +47,7 @@ export const AddComment = React.memo<AddCommentProps>(
5347
options: { stripEmptyFields: false },
5448
schema,
5549
});
56-
const dispatch = useDispatch();
57-
const apolloClient = useApolloClient();
50+
5851
const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline<CommentRequest>(
5952
form,
6053
'comment'
@@ -68,30 +61,9 @@ export const AddComment = React.memo<AddCommentProps>(
6861
`${comment}${comment.length > 0 ? '\n\n' : ''}${insertQuote}`
6962
);
7063
}
71-
// eslint-disable-next-line react-hooks/exhaustive-deps
72-
}, [insertQuote]);
64+
}, [form, insertQuote]);
7365

74-
const handleTimelineClick = useCallback(
75-
(timelineId: string) => {
76-
queryTimelineById({
77-
apolloClient,
78-
timelineId,
79-
updateIsLoading: ({
80-
id: currentTimelineId,
81-
isLoading: isLoadingTimeline,
82-
}: {
83-
id: string;
84-
isLoading: boolean;
85-
}) =>
86-
dispatch(
87-
dispatchUpdateIsLoading({ id: currentTimelineId, isLoading: isLoadingTimeline })
88-
),
89-
updateTimeline: dispatchUpdateTimeline(dispatch),
90-
});
91-
},
92-
// eslint-disable-next-line react-hooks/exhaustive-deps
93-
[apolloClient]
94-
);
66+
const handleTimelineClick = useTimelineClick();
9567

9668
const onSubmit = useCallback(async () => {
9769
const { isValid, data } = await form.submit();
@@ -102,8 +74,8 @@ export const AddComment = React.memo<AddCommentProps>(
10274
postComment(data, onCommentPosted);
10375
form.reset();
10476
}
105-
// eslint-disable-next-line react-hooks/exhaustive-deps
106-
}, [form, onCommentPosted, onCommentSaving]);
77+
}, [form, onCommentPosted, onCommentSaving, postComment]);
78+
10779
return (
10880
<span id="add-comment-permLink">
10981
{isLoading && showLoading && <MySpinner data-test-subj="loading-spinner" size="xl" />}

x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,6 @@ const useGetCasesMock = useGetCases as jest.Mock;
2929
const useGetCasesStatusMock = useGetCasesStatus as jest.Mock;
3030
const useUpdateCasesMock = useUpdateCases as jest.Mock;
3131

32-
jest.mock('react-router-dom', () => {
33-
const originalModule = jest.requireActual('react-router-dom');
34-
return {
35-
...originalModule,
36-
useHistory: jest.fn(),
37-
};
38-
});
39-
4032
jest.mock('../../../common/components/link_to');
4133

4234
describe('AllCases', () => {

x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
*/
66
/* eslint-disable react-hooks/exhaustive-deps */
77
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
8-
import { useHistory } from 'react-router-dom';
98
import {
109
EuiBasicTable,
1110
EuiContextMenuPanel,
@@ -50,6 +49,8 @@ import { ConfigureCaseButton } from '../configure_cases/button';
5049
import { ERROR_PUSH_SERVICE_CALLOUT_TITLE } from '../use_push_to_service/translations';
5150
import { LinkButton } from '../../../common/components/links';
5251
import { SecurityPageName } from '../../../app/types';
52+
import { useKibana } from '../../../common/lib/kibana';
53+
import { APP_ID } from '../../../../common/constants';
5354

5455
const Div = styled.div`
5556
margin-top: ${({ theme }) => theme.eui.paddingSizes.m};
@@ -81,13 +82,13 @@ const getSortField = (field: string): SortFieldCase => {
8182
};
8283

8384
interface AllCasesProps {
84-
onRowClick?: (id: string) => void;
85+
onRowClick?: (id?: string) => void;
8586
isModal?: boolean;
8687
userCanCrud: boolean;
8788
}
8889
export const AllCases = React.memo<AllCasesProps>(
89-
({ onRowClick = () => {}, isModal = false, userCanCrud }) => {
90-
const history = useHistory();
90+
({ onRowClick, isModal = false, userCanCrud }) => {
91+
const { navigateToApp } = useKibana().services.application;
9192
const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.case);
9293
const { actionLicense } = useGetActionLicense();
9394
const {
@@ -234,9 +235,15 @@ export const AllCases = React.memo<AllCasesProps>(
234235
const goToCreateCase = useCallback(
235236
(ev) => {
236237
ev.preventDefault();
237-
history.push(getCreateCaseUrl(urlSearch));
238+
if (isModal && onRowClick != null) {
239+
onRowClick();
240+
} else {
241+
navigateToApp(`${APP_ID}:${SecurityPageName.case}`, {
242+
path: getCreateCaseUrl(urlSearch),
243+
});
244+
}
238245
},
239-
[history, urlSearch]
246+
[navigateToApp, isModal, onRowClick, urlSearch]
240247
);
241248

242249
const actions = useMemo(
@@ -445,7 +452,11 @@ export const AllCases = React.memo<AllCasesProps>(
445452
rowProps={(item) =>
446453
isModal
447454
? {
448-
onClick: () => onRowClick(item.id),
455+
onClick: () => {
456+
if (onRowClick != null) {
457+
onRowClick(item.id);
458+
}
459+
},
449460
}
450461
: {}
451462
}

x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import * as i18n from './translations';
1919
interface AllCasesModalProps {
2020
onCloseCaseModal: () => void;
2121
showCaseModal: boolean;
22-
onRowClick: (id: string) => void;
22+
onRowClick: (id?: string) => void;
2323
}
2424

2525
export const AllCasesModalComponent = ({

x-pack/plugins/security_solution/public/cases/components/create/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import * as i18n from '../../translations';
3333
import { MarkdownEditorForm } from '../../../common/components//markdown_editor/form';
3434
import { useGetTags } from '../../containers/use_get_tags';
3535
import { getCaseDetailsUrl } from '../../../common/components/link_to';
36+
import { useTimelineClick } from '../utils/use_timeline_click';
3637

3738
export const CommonUseField = getUseField({ component: Field });
3839

@@ -87,15 +88,15 @@ export const Create = React.memo(() => {
8788
form,
8889
'description'
8990
);
91+
const handleTimelineClick = useTimelineClick();
9092

9193
const onSubmit = useCallback(async () => {
9294
const { isValid, data } = await form.submit();
9395
if (isValid) {
9496
// `postCase`'s type is incorrect, it actually returns a promise
9597
await postCase(data);
9698
}
97-
// eslint-disable-next-line react-hooks/exhaustive-deps
98-
}, [form]);
99+
}, [form, postCase]);
99100

100101
const handleSetIsCancel = useCallback(() => {
101102
history.push('/');
@@ -145,6 +146,7 @@ export const Create = React.memo(() => {
145146
dataTestSubj: 'caseDescription',
146147
idAria: 'caseDescription',
147148
isDisabled: isLoading,
149+
onClickTimeline: handleTimelineClick,
148150
onCursorPositionUpdate: handleCursorChange,
149151
topRightContent: (
150152
<InsertTimelinePopover

x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ describe('UserActionMarkdown ', () => {
4444

4545
expect(queryTimelineByIdSpy).toBeCalledWith({
4646
apolloClient: mockUseApolloClient(),
47+
graphEventId: '',
4748
timelineId,
4849
updateIsLoading: expect.any(Function),
4950
updateTimeline: expect.any(Function),
@@ -62,6 +63,7 @@ describe('UserActionMarkdown ', () => {
6263
wrapper.find(`[data-test-subj="markdown-timeline-link"]`).first().simulate('click');
6364
expect(queryTimelineByIdSpy).toBeCalledWith({
6465
apolloClient: mockUseApolloClient(),
66+
graphEventId: '',
6567
timelineId,
6668
updateIsLoading: expect.any(Function),
6769
updateTimeline: expect.any(Function),

x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,14 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/e
88
import React, { useCallback } from 'react';
99
import styled, { css } from 'styled-components';
1010

11-
import { useDispatch } from 'react-redux';
1211
import * as i18n from '../case_view/translations';
1312
import { Markdown } from '../../../common/components/markdown';
1413
import { Form, useForm, UseField } from '../../../shared_imports';
1514
import { schema, Content } from './schema';
1615
import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover';
1716
import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline';
1817
import { MarkdownEditorForm } from '../../../common/components//markdown_editor/form';
19-
import {
20-
dispatchUpdateTimeline,
21-
queryTimelineById,
22-
} from '../../../timelines/components/open_timeline/helpers';
23-
24-
import { updateIsLoading as dispatchUpdateIsLoading } from '../../../timelines/store/timeline/actions';
25-
import { useApolloClient } from '../../../common/utils/apollo_context';
18+
import { useTimelineClick } from '../utils/use_timeline_click';
2619

2720
const ContentWrapper = styled.div`
2821
${({ theme }) => css`
@@ -44,8 +37,6 @@ export const UserActionMarkdown = ({
4437
onChangeEditable,
4538
onSaveContent,
4639
}: UserActionMarkdownProps) => {
47-
const dispatch = useDispatch();
48-
const apolloClient = useApolloClient();
4940
const { form } = useForm<Content>({
5041
defaultValue: { content },
5142
options: { stripEmptyFields: false },
@@ -59,24 +50,7 @@ export const UserActionMarkdown = ({
5950
onChangeEditable(id);
6051
}, [id, onChangeEditable]);
6152

62-
const handleTimelineClick = useCallback(
63-
(timelineId: string) => {
64-
queryTimelineById({
65-
apolloClient,
66-
timelineId,
67-
updateIsLoading: ({
68-
id: currentTimelineId,
69-
isLoading,
70-
}: {
71-
id: string;
72-
isLoading: boolean;
73-
}) => dispatch(dispatchUpdateIsLoading({ id: currentTimelineId, isLoading })),
74-
updateTimeline: dispatchUpdateTimeline(dispatch),
75-
});
76-
},
77-
// eslint-disable-next-line react-hooks/exhaustive-deps
78-
[apolloClient]
79-
);
53+
const handleTimelineClick = useTimelineClick();
8054

8155
const handleSaveAction = useCallback(async () => {
8256
const { isValid, data } = await form.submit();
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { useCallback } from 'react';
8+
import { useDispatch } from 'react-redux';
9+
import { useApolloClient } from '../../../common/utils/apollo_context';
10+
import {
11+
dispatchUpdateTimeline,
12+
queryTimelineById,
13+
} from '../../../timelines/components/open_timeline/helpers';
14+
import { updateIsLoading as dispatchUpdateIsLoading } from '../../../timelines/store/timeline/actions';
15+
16+
export const useTimelineClick = () => {
17+
const dispatch = useDispatch();
18+
const apolloClient = useApolloClient();
19+
20+
const handleTimelineClick = useCallback(
21+
(timelineId: string, graphEventId?: string) => {
22+
queryTimelineById({
23+
apolloClient,
24+
graphEventId,
25+
timelineId,
26+
updateIsLoading: ({
27+
id: currentTimelineId,
28+
isLoading,
29+
}: {
30+
id: string;
31+
isLoading: boolean;
32+
}) => dispatch(dispatchUpdateIsLoading({ id: currentTimelineId, isLoading })),
33+
updateTimeline: dispatchUpdateTimeline(dispatch),
34+
});
35+
},
36+
[apolloClient, dispatch]
37+
);
38+
39+
return handleTimelineClick;
40+
};

x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,7 @@ const EventsViewerComponent: React.FC<Props> = ({
106106

107107
useEffect(() => {
108108
setIsTimelineLoading({ id, isLoading: isQueryLoading });
109-
// eslint-disable-next-line react-hooks/exhaustive-deps
110-
}, [isQueryLoading]);
109+
}, [id, isQueryLoading, setIsTimelineLoading]);
111110

112111
const { queryFields, title, unit } = useMemo(() => getManageTimelineById(id), [
113112
getManageTimelineById,

x-pack/plugins/security_solution/public/common/components/markdown/index.test.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,19 @@ describe('Markdown', () => {
157157
);
158158
wrapper.find('[data-test-subj="markdown-timeline-link"]').first().simulate('click');
159159

160-
expect(onClickTimeline).toHaveBeenCalledWith(timelineId);
160+
expect(onClickTimeline).toHaveBeenCalledWith(timelineId, '');
161+
});
162+
163+
test('timeline link onClick calls onClickTimeline with timelineId and graphEventId', () => {
164+
const graphEventId = '2bc51864784c';
165+
const markdownWithTimelineAndGraphEventLink = `A link to a timeline [timeline](http://localhost:5601/app/siem#/timelines?timeline=(id:'${timelineId}',isOpen:!t,graphEventId:'${graphEventId}'))`;
166+
167+
const wrapper = mount(
168+
<Markdown raw={markdownWithTimelineAndGraphEventLink} onClickTimeline={onClickTimeline} />
169+
);
170+
wrapper.find('[data-test-subj="markdown-timeline-link"]').first().simulate('click');
171+
172+
expect(onClickTimeline).toHaveBeenCalledWith(timelineId, graphEventId);
161173
});
162174
});
163175
});

0 commit comments

Comments
 (0)