[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>
This commit is contained in:
parent
667b72f9e8
commit
75582eb4ae
|
@ -8,7 +8,6 @@ import { EuiButton, EuiLoadingSpinner } from '@elastic/eui';
|
|||
import React, { useCallback, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { CommentRequest } from '../../../../../case/common/api';
|
||||
import { usePostComment } from '../../containers/use_post_comment';
|
||||
import { Case } from '../../containers/types';
|
||||
|
@ -19,12 +18,7 @@ import { Form, useForm, UseField } from '../../../shared_imports';
|
|||
|
||||
import * as i18n from './translations';
|
||||
import { schema } from './schema';
|
||||
import {
|
||||
dispatchUpdateTimeline,
|
||||
queryTimelineById,
|
||||
} from '../../../timelines/components/open_timeline/helpers';
|
||||
import { updateIsLoading as dispatchUpdateIsLoading } from '../../../timelines/store/timeline/actions';
|
||||
import { useApolloClient } from '../../../common/utils/apollo_context';
|
||||
import { useTimelineClick } from '../utils/use_timeline_click';
|
||||
|
||||
const MySpinner = styled(EuiLoadingSpinner)`
|
||||
position: absolute;
|
||||
|
@ -53,8 +47,7 @@ export const AddComment = React.memo<AddCommentProps>(
|
|||
options: { stripEmptyFields: false },
|
||||
schema,
|
||||
});
|
||||
const dispatch = useDispatch();
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline<CommentRequest>(
|
||||
form,
|
||||
'comment'
|
||||
|
@ -68,30 +61,9 @@ export const AddComment = React.memo<AddCommentProps>(
|
|||
`${comment}${comment.length > 0 ? '\n\n' : ''}${insertQuote}`
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [insertQuote]);
|
||||
}, [form, insertQuote]);
|
||||
|
||||
const handleTimelineClick = useCallback(
|
||||
(timelineId: string) => {
|
||||
queryTimelineById({
|
||||
apolloClient,
|
||||
timelineId,
|
||||
updateIsLoading: ({
|
||||
id: currentTimelineId,
|
||||
isLoading: isLoadingTimeline,
|
||||
}: {
|
||||
id: string;
|
||||
isLoading: boolean;
|
||||
}) =>
|
||||
dispatch(
|
||||
dispatchUpdateIsLoading({ id: currentTimelineId, isLoading: isLoadingTimeline })
|
||||
),
|
||||
updateTimeline: dispatchUpdateTimeline(dispatch),
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[apolloClient]
|
||||
);
|
||||
const handleTimelineClick = useTimelineClick();
|
||||
|
||||
const onSubmit = useCallback(async () => {
|
||||
const { isValid, data } = await form.submit();
|
||||
|
@ -102,8 +74,8 @@ export const AddComment = React.memo<AddCommentProps>(
|
|||
postComment(data, onCommentPosted);
|
||||
form.reset();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [form, onCommentPosted, onCommentSaving]);
|
||||
}, [form, onCommentPosted, onCommentSaving, postComment]);
|
||||
|
||||
return (
|
||||
<span id="add-comment-permLink">
|
||||
{isLoading && showLoading && <MySpinner data-test-subj="loading-spinner" size="xl" />}
|
||||
|
|
|
@ -29,14 +29,6 @@ const useGetCasesMock = useGetCases as jest.Mock;
|
|||
const useGetCasesStatusMock = useGetCasesStatus as jest.Mock;
|
||||
const useUpdateCasesMock = useUpdateCases as jest.Mock;
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const originalModule = jest.requireActual('react-router-dom');
|
||||
return {
|
||||
...originalModule,
|
||||
useHistory: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../common/components/link_to');
|
||||
|
||||
describe('AllCases', () => {
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiContextMenuPanel,
|
||||
|
@ -50,6 +49,8 @@ import { ConfigureCaseButton } from '../configure_cases/button';
|
|||
import { ERROR_PUSH_SERVICE_CALLOUT_TITLE } from '../use_push_to_service/translations';
|
||||
import { LinkButton } from '../../../common/components/links';
|
||||
import { SecurityPageName } from '../../../app/types';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { APP_ID } from '../../../../common/constants';
|
||||
|
||||
const Div = styled.div`
|
||||
margin-top: ${({ theme }) => theme.eui.paddingSizes.m};
|
||||
|
@ -81,13 +82,13 @@ const getSortField = (field: string): SortFieldCase => {
|
|||
};
|
||||
|
||||
interface AllCasesProps {
|
||||
onRowClick?: (id: string) => void;
|
||||
onRowClick?: (id?: string) => void;
|
||||
isModal?: boolean;
|
||||
userCanCrud: boolean;
|
||||
}
|
||||
export const AllCases = React.memo<AllCasesProps>(
|
||||
({ onRowClick = () => {}, isModal = false, userCanCrud }) => {
|
||||
const history = useHistory();
|
||||
({ onRowClick, isModal = false, userCanCrud }) => {
|
||||
const { navigateToApp } = useKibana().services.application;
|
||||
const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.case);
|
||||
const { actionLicense } = useGetActionLicense();
|
||||
const {
|
||||
|
@ -234,9 +235,15 @@ export const AllCases = React.memo<AllCasesProps>(
|
|||
const goToCreateCase = useCallback(
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
history.push(getCreateCaseUrl(urlSearch));
|
||||
if (isModal && onRowClick != null) {
|
||||
onRowClick();
|
||||
} else {
|
||||
navigateToApp(`${APP_ID}:${SecurityPageName.case}`, {
|
||||
path: getCreateCaseUrl(urlSearch),
|
||||
});
|
||||
}
|
||||
},
|
||||
[history, urlSearch]
|
||||
[navigateToApp, isModal, onRowClick, urlSearch]
|
||||
);
|
||||
|
||||
const actions = useMemo(
|
||||
|
@ -445,7 +452,11 @@ export const AllCases = React.memo<AllCasesProps>(
|
|||
rowProps={(item) =>
|
||||
isModal
|
||||
? {
|
||||
onClick: () => onRowClick(item.id),
|
||||
onClick: () => {
|
||||
if (onRowClick != null) {
|
||||
onRowClick(item.id);
|
||||
}
|
||||
},
|
||||
}
|
||||
: {}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import * as i18n from './translations';
|
|||
interface AllCasesModalProps {
|
||||
onCloseCaseModal: () => void;
|
||||
showCaseModal: boolean;
|
||||
onRowClick: (id: string) => void;
|
||||
onRowClick: (id?: string) => void;
|
||||
}
|
||||
|
||||
export const AllCasesModalComponent = ({
|
||||
|
|
|
@ -33,6 +33,7 @@ import * as i18n from '../../translations';
|
|||
import { MarkdownEditorForm } from '../../../common/components//markdown_editor/form';
|
||||
import { useGetTags } from '../../containers/use_get_tags';
|
||||
import { getCaseDetailsUrl } from '../../../common/components/link_to';
|
||||
import { useTimelineClick } from '../utils/use_timeline_click';
|
||||
|
||||
export const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
|
@ -87,6 +88,7 @@ export const Create = React.memo(() => {
|
|||
form,
|
||||
'description'
|
||||
);
|
||||
const handleTimelineClick = useTimelineClick();
|
||||
|
||||
const onSubmit = useCallback(async () => {
|
||||
const { isValid, data } = await form.submit();
|
||||
|
@ -94,8 +96,7 @@ export const Create = React.memo(() => {
|
|||
// `postCase`'s type is incorrect, it actually returns a promise
|
||||
await postCase(data);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [form]);
|
||||
}, [form, postCase]);
|
||||
|
||||
const handleSetIsCancel = useCallback(() => {
|
||||
history.push('/');
|
||||
|
@ -145,6 +146,7 @@ export const Create = React.memo(() => {
|
|||
dataTestSubj: 'caseDescription',
|
||||
idAria: 'caseDescription',
|
||||
isDisabled: isLoading,
|
||||
onClickTimeline: handleTimelineClick,
|
||||
onCursorPositionUpdate: handleCursorChange,
|
||||
topRightContent: (
|
||||
<InsertTimelinePopover
|
||||
|
|
|
@ -44,6 +44,7 @@ describe('UserActionMarkdown ', () => {
|
|||
|
||||
expect(queryTimelineByIdSpy).toBeCalledWith({
|
||||
apolloClient: mockUseApolloClient(),
|
||||
graphEventId: '',
|
||||
timelineId,
|
||||
updateIsLoading: expect.any(Function),
|
||||
updateTimeline: expect.any(Function),
|
||||
|
@ -62,6 +63,7 @@ describe('UserActionMarkdown ', () => {
|
|||
wrapper.find(`[data-test-subj="markdown-timeline-link"]`).first().simulate('click');
|
||||
expect(queryTimelineByIdSpy).toBeCalledWith({
|
||||
apolloClient: mockUseApolloClient(),
|
||||
graphEventId: '',
|
||||
timelineId,
|
||||
updateIsLoading: expect.any(Function),
|
||||
updateTimeline: expect.any(Function),
|
||||
|
|
|
@ -8,7 +8,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/e
|
|||
import React, { useCallback } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
import * as i18n from '../case_view/translations';
|
||||
import { Markdown } from '../../../common/components/markdown';
|
||||
import { Form, useForm, UseField } from '../../../shared_imports';
|
||||
|
@ -16,13 +15,7 @@ import { schema, Content } from './schema';
|
|||
import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover';
|
||||
import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline';
|
||||
import { MarkdownEditorForm } from '../../../common/components//markdown_editor/form';
|
||||
import {
|
||||
dispatchUpdateTimeline,
|
||||
queryTimelineById,
|
||||
} from '../../../timelines/components/open_timeline/helpers';
|
||||
|
||||
import { updateIsLoading as dispatchUpdateIsLoading } from '../../../timelines/store/timeline/actions';
|
||||
import { useApolloClient } from '../../../common/utils/apollo_context';
|
||||
import { useTimelineClick } from '../utils/use_timeline_click';
|
||||
|
||||
const ContentWrapper = styled.div`
|
||||
${({ theme }) => css`
|
||||
|
@ -44,8 +37,6 @@ export const UserActionMarkdown = ({
|
|||
onChangeEditable,
|
||||
onSaveContent,
|
||||
}: UserActionMarkdownProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const apolloClient = useApolloClient();
|
||||
const { form } = useForm<Content>({
|
||||
defaultValue: { content },
|
||||
options: { stripEmptyFields: false },
|
||||
|
@ -59,24 +50,7 @@ export const UserActionMarkdown = ({
|
|||
onChangeEditable(id);
|
||||
}, [id, onChangeEditable]);
|
||||
|
||||
const handleTimelineClick = useCallback(
|
||||
(timelineId: string) => {
|
||||
queryTimelineById({
|
||||
apolloClient,
|
||||
timelineId,
|
||||
updateIsLoading: ({
|
||||
id: currentTimelineId,
|
||||
isLoading,
|
||||
}: {
|
||||
id: string;
|
||||
isLoading: boolean;
|
||||
}) => dispatch(dispatchUpdateIsLoading({ id: currentTimelineId, isLoading })),
|
||||
updateTimeline: dispatchUpdateTimeline(dispatch),
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[apolloClient]
|
||||
);
|
||||
const handleTimelineClick = useTimelineClick();
|
||||
|
||||
const handleSaveAction = useCallback(async () => {
|
||||
const { isValid, data } = await form.submit();
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useApolloClient } from '../../../common/utils/apollo_context';
|
||||
import {
|
||||
dispatchUpdateTimeline,
|
||||
queryTimelineById,
|
||||
} from '../../../timelines/components/open_timeline/helpers';
|
||||
import { updateIsLoading as dispatchUpdateIsLoading } from '../../../timelines/store/timeline/actions';
|
||||
|
||||
export const useTimelineClick = () => {
|
||||
const dispatch = useDispatch();
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const handleTimelineClick = useCallback(
|
||||
(timelineId: string, graphEventId?: string) => {
|
||||
queryTimelineById({
|
||||
apolloClient,
|
||||
graphEventId,
|
||||
timelineId,
|
||||
updateIsLoading: ({
|
||||
id: currentTimelineId,
|
||||
isLoading,
|
||||
}: {
|
||||
id: string;
|
||||
isLoading: boolean;
|
||||
}) => dispatch(dispatchUpdateIsLoading({ id: currentTimelineId, isLoading })),
|
||||
updateTimeline: dispatchUpdateTimeline(dispatch),
|
||||
});
|
||||
},
|
||||
[apolloClient, dispatch]
|
||||
);
|
||||
|
||||
return handleTimelineClick;
|
||||
};
|
|
@ -106,8 +106,7 @@ const EventsViewerComponent: React.FC<Props> = ({
|
|||
|
||||
useEffect(() => {
|
||||
setIsTimelineLoading({ id, isLoading: isQueryLoading });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isQueryLoading]);
|
||||
}, [id, isQueryLoading, setIsTimelineLoading]);
|
||||
|
||||
const { queryFields, title, unit } = useMemo(() => getManageTimelineById(id), [
|
||||
getManageTimelineById,
|
||||
|
|
|
@ -157,7 +157,19 @@ describe('Markdown', () => {
|
|||
);
|
||||
wrapper.find('[data-test-subj="markdown-timeline-link"]').first().simulate('click');
|
||||
|
||||
expect(onClickTimeline).toHaveBeenCalledWith(timelineId);
|
||||
expect(onClickTimeline).toHaveBeenCalledWith(timelineId, '');
|
||||
});
|
||||
|
||||
test('timeline link onClick calls onClickTimeline with timelineId and graphEventId', () => {
|
||||
const graphEventId = '2bc51864784c';
|
||||
const markdownWithTimelineAndGraphEventLink = `A link to a timeline [timeline](http://localhost:5601/app/siem#/timelines?timeline=(id:'${timelineId}',isOpen:!t,graphEventId:'${graphEventId}'))`;
|
||||
|
||||
const wrapper = mount(
|
||||
<Markdown raw={markdownWithTimelineAndGraphEventLink} onClickTimeline={onClickTimeline} />
|
||||
);
|
||||
wrapper.find('[data-test-subj="markdown-timeline-link"]').first().simulate('click');
|
||||
|
||||
expect(onClickTimeline).toHaveBeenCalledWith(timelineId, graphEventId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
/* eslint-disable react/display-name */
|
||||
|
||||
import { EuiLink, EuiTableRow, EuiTableRowCell, EuiText, EuiToolTip } from '@elastic/eui';
|
||||
import { clone } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
@ -38,7 +39,7 @@ const REL_NOREFERRER = 'noreferrer';
|
|||
export const Markdown = React.memo<{
|
||||
disableLinks?: boolean;
|
||||
raw?: string;
|
||||
onClickTimeline?: (timelineId: string) => void;
|
||||
onClickTimeline?: (timelineId: string, graphEventId?: string) => void;
|
||||
size?: 'xs' | 's' | 'm';
|
||||
}>(({ disableLinks = false, onClickTimeline, raw, size = 's' }) => {
|
||||
const markdownRenderers = {
|
||||
|
@ -63,11 +64,14 @@ export const Markdown = React.memo<{
|
|||
),
|
||||
link: ({ children, href }: { children: React.ReactNode[]; href?: string }) => {
|
||||
if (onClickTimeline != null && href != null && href.indexOf(`timelines?timeline=(id:`) > -1) {
|
||||
const timelineId = href.split('timelines?timeline=(id:')[1].split("'")[1] ?? '';
|
||||
const timelineId = clone(href).split('timeline=(id:')[1].split("'")[1] ?? '';
|
||||
const graphEventId = href.includes('graphEventId:')
|
||||
? clone(href).split('graphEventId:')[1].split("'")[1] ?? ''
|
||||
: '';
|
||||
return (
|
||||
<EuiToolTip content={i18n.TIMELINE_ID(timelineId)}>
|
||||
<EuiLink
|
||||
onClick={() => onClickTimeline(timelineId)}
|
||||
onClick={() => onClickTimeline(timelineId, graphEventId)}
|
||||
data-test-subj="markdown-timeline-link"
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -16,7 +16,7 @@ interface IMarkdownEditorForm {
|
|||
field: FieldHook;
|
||||
idAria: string;
|
||||
isDisabled: boolean;
|
||||
onClickTimeline?: (timelineId: string) => void;
|
||||
onClickTimeline?: (timelineId: string, graphEventId?: string) => void;
|
||||
onCursorPositionUpdate?: (cursorPosition: CursorPosition) => void;
|
||||
placeholder?: string;
|
||||
topRightContent?: React.ReactNode;
|
||||
|
|
|
@ -74,7 +74,7 @@ export const MarkdownEditor = React.memo<{
|
|||
content: string;
|
||||
isDisabled?: boolean;
|
||||
onChange: (description: string) => void;
|
||||
onClickTimeline?: (timelineId: string) => void;
|
||||
onClickTimeline?: (timelineId: string, graphEventId?: string) => void;
|
||||
onCursorPositionUpdate?: (cursorPosition: CursorPosition) => void;
|
||||
placeholder?: string;
|
||||
}>(
|
||||
|
@ -95,15 +95,18 @@ export const MarkdownEditor = React.memo<{
|
|||
[onChange]
|
||||
);
|
||||
|
||||
const setCursorPosition = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
if (onCursorPositionUpdate) {
|
||||
onCursorPositionUpdate({
|
||||
start: e!.target!.selectionStart ?? 0,
|
||||
end: e!.target!.selectionEnd ?? 0,
|
||||
});
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const setCursorPosition = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
if (onCursorPositionUpdate) {
|
||||
onCursorPositionUpdate({
|
||||
start: e!.target!.selectionStart ?? 0,
|
||||
end: e!.target!.selectionEnd ?? 0,
|
||||
});
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[onCursorPositionUpdate]
|
||||
);
|
||||
|
||||
const tabs = useMemo(
|
||||
() => [
|
||||
|
@ -135,8 +138,7 @@ export const MarkdownEditor = React.memo<{
|
|||
),
|
||||
},
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[content, isDisabled, placeholder]
|
||||
[content, handleOnChange, isDisabled, onClickTimeline, placeholder, setCursorPosition]
|
||||
);
|
||||
return (
|
||||
<Container>
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
getTitle,
|
||||
replaceStateInLocation,
|
||||
updateUrlStateString,
|
||||
decodeRisonUrlState,
|
||||
} from './helpers';
|
||||
import {
|
||||
UrlStateContainerPropTypes,
|
||||
|
@ -26,8 +27,10 @@ import {
|
|||
KeyUrlState,
|
||||
ALL_URL_STATE_KEYS,
|
||||
UrlStateToRedux,
|
||||
UrlState,
|
||||
} from './types';
|
||||
import { SecurityPageName } from '../../../app/types';
|
||||
import { TimelineUrl } from '../../../timelines/store/timeline/model';
|
||||
|
||||
function usePrevious(value: PreviousLocationUrlState) {
|
||||
const ref = useRef<PreviousLocationUrlState>(value);
|
||||
|
@ -37,6 +40,21 @@ function usePrevious(value: PreviousLocationUrlState) {
|
|||
return ref.current;
|
||||
}
|
||||
|
||||
const updateTimelineAtinitialization = (
|
||||
urlKey: CONSTANTS,
|
||||
newUrlStateString: string,
|
||||
urlState: UrlState
|
||||
) => {
|
||||
let updateUrlState = true;
|
||||
if (urlKey === CONSTANTS.timeline) {
|
||||
const timeline = decodeRisonUrlState<TimelineUrl>(newUrlStateString);
|
||||
if (timeline != null && urlState.timeline.id === timeline.id) {
|
||||
updateUrlState = false;
|
||||
}
|
||||
}
|
||||
return updateUrlState;
|
||||
};
|
||||
|
||||
export const useUrlStateHooks = ({
|
||||
detailName,
|
||||
indexPattern,
|
||||
|
@ -78,13 +96,15 @@ export const useUrlStateHooks = ({
|
|||
getParamFromQueryString(getQueryStringFromLocation(mySearch), urlKey) ??
|
||||
newUrlStateString;
|
||||
if (isInitializing || !deepEqual(updatedUrlStateString, newUrlStateString)) {
|
||||
urlStateToUpdate = [
|
||||
...urlStateToUpdate,
|
||||
{
|
||||
urlKey,
|
||||
newUrlStateString: updatedUrlStateString,
|
||||
},
|
||||
];
|
||||
if (updateTimelineAtinitialization(urlKey, newUrlStateString, urlState)) {
|
||||
urlStateToUpdate = [
|
||||
...urlStateToUpdate,
|
||||
{
|
||||
urlKey,
|
||||
newUrlStateString: updatedUrlStateString,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
|
|
|
@ -17,6 +17,10 @@ const WithHoverActionsPopover = (styled(EuiPopover as any)`
|
|||
}
|
||||
` as unknown) as typeof EuiPopover;
|
||||
|
||||
const Container = styled.div`
|
||||
width: fit-content;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Always show the hover menu contents (default: false)
|
||||
|
@ -75,7 +79,7 @@ export const WithHoverActions = React.memo<Props>(
|
|||
}, [closePopOverTrigger]);
|
||||
|
||||
return (
|
||||
<div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<Container onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<WithHoverActionsPopover
|
||||
anchorPosition={'downCenter'}
|
||||
button={content}
|
||||
|
@ -86,7 +90,7 @@ export const WithHoverActions = React.memo<Props>(
|
|||
>
|
||||
{isOpen ? <>{hoverContent}</> : null}
|
||||
</WithHoverActionsPopover>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -374,7 +374,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
}
|
||||
}, [defaultFilters, filterGroup]);
|
||||
const { filterManager } = useKibana().services.data.query;
|
||||
const { initializeTimeline, setTimelineRowActions } = useManageTimeline();
|
||||
const { initializeTimeline, setTimelineRowActions, setIndexToAdd } = useManageTimeline();
|
||||
|
||||
useEffect(() => {
|
||||
initializeTimeline({
|
||||
|
@ -383,6 +383,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
filterManager,
|
||||
footerText: i18n.TOTAL_COUNT_OF_ALERTS,
|
||||
id: timelineId,
|
||||
indexToAdd: defaultIndices,
|
||||
loadingText: i18n.LOADING_ALERTS,
|
||||
selectAll: canUserCRUD ? selectAll : false,
|
||||
timelineRowActions: () => [getInvestigateInResolverAction({ dispatch, timelineId })],
|
||||
|
@ -390,6 +391,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setTimelineRowActions({
|
||||
id: timelineId,
|
||||
|
@ -398,6 +400,11 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [additionalActions]);
|
||||
|
||||
useEffect(() => {
|
||||
setIndexToAdd({ id: timelineId, indexToAdd: defaultIndices });
|
||||
}, [timelineId, defaultIndices, setIndexToAdd]);
|
||||
|
||||
const headerFilterGroup = useMemo(
|
||||
() => <AlertsTableFilterGroup onFilterGroupChanged={onFilterGroupChangedCallback} />,
|
||||
[onFilterGroupChangedCallback]
|
||||
|
|
|
@ -31,6 +31,7 @@ const EuiFlyoutContainer = styled.div`
|
|||
z-index: 4001;
|
||||
min-width: 150px;
|
||||
width: auto;
|
||||
animation: none;
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import styled from 'styled-components';
|
|||
|
||||
import { SecurityPageName } from '../../../app/types';
|
||||
import { AllCasesModal } from '../../../cases/components/all_cases_modal';
|
||||
import { getCaseDetailsUrl } from '../../../common/components/link_to';
|
||||
import { getCaseDetailsUrl, getCreateCaseUrl } from '../../../common/components/link_to';
|
||||
import { APP_ID } from '../../../../common/constants';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { State } from '../../../common/store';
|
||||
|
@ -28,6 +28,7 @@ import {
|
|||
import { Resolver } from '../../../resolver/view';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { TimelineType } from '../../../../common/types/timeline';
|
||||
|
||||
const OverlayContainer = styled.div<{ bodyHeight?: number }>`
|
||||
height: ${({ bodyHeight }) => (bodyHeight ? `${bodyHeight}px` : 'auto')};
|
||||
|
@ -44,6 +45,7 @@ interface OwnProps {
|
|||
bodyHeight?: number;
|
||||
graphEventId?: string;
|
||||
timelineId: string;
|
||||
timelineType: TimelineType;
|
||||
}
|
||||
|
||||
const GraphOverlayComponent = ({
|
||||
|
@ -52,6 +54,7 @@ const GraphOverlayComponent = ({
|
|||
status,
|
||||
timelineId,
|
||||
title,
|
||||
timelineType,
|
||||
}: OwnProps & PropsFromRedux) => {
|
||||
const dispatch = useDispatch();
|
||||
const { navigateToApp } = useKibana().services.application;
|
||||
|
@ -65,20 +68,20 @@ const GraphOverlayComponent = ({
|
|||
timelineSelectors.selectTimeline(state, timelineId)
|
||||
);
|
||||
const onRowClick = useCallback(
|
||||
(id: string) => {
|
||||
(id?: string) => {
|
||||
onCloseCaseModal();
|
||||
|
||||
dispatch(
|
||||
setInsertTimeline({
|
||||
graphEventId,
|
||||
timelineId,
|
||||
timelineSavedObjectId: currentTimeline.savedObjectId,
|
||||
timelineTitle: title.length > 0 ? title : UNTITLED_TIMELINE,
|
||||
})
|
||||
);
|
||||
|
||||
navigateToApp(`${APP_ID}:${SecurityPageName.case}`, {
|
||||
path: getCaseDetailsUrl({ id }),
|
||||
path: id != null ? getCaseDetailsUrl({ id }) : getCreateCaseUrl(),
|
||||
}).then(() => {
|
||||
dispatch(
|
||||
setInsertTimeline({
|
||||
graphEventId,
|
||||
timelineId,
|
||||
timelineSavedObjectId: currentTimeline.savedObjectId,
|
||||
timelineTitle: title.length > 0 ? title : UNTITLED_TIMELINE,
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
[currentTimeline, dispatch, graphEventId, navigateToApp, onCloseCaseModal, timelineId, title]
|
||||
|
@ -93,28 +96,30 @@ const GraphOverlayComponent = ({
|
|||
{i18n.BACK_TO_EVENTS}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<NewCase
|
||||
compact={true}
|
||||
graphEventId={graphEventId}
|
||||
onClosePopover={noop}
|
||||
timelineId={timelineId}
|
||||
timelineTitle={title}
|
||||
timelineStatus={status}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ExistingCase
|
||||
compact={true}
|
||||
onClosePopover={noop}
|
||||
onOpenCaseModal={onOpenCaseModal}
|
||||
timelineStatus={status}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{timelineType === TimelineType.default && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<NewCase
|
||||
compact={true}
|
||||
graphEventId={graphEventId}
|
||||
onClosePopover={noop}
|
||||
timelineId={timelineId}
|
||||
timelineTitle={title}
|
||||
timelineStatus={status}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ExistingCase
|
||||
compact={true}
|
||||
onClosePopover={noop}
|
||||
onOpenCaseModal={onOpenCaseModal}
|
||||
timelineStatus={status}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiHorizontalRule margin="none" />
|
||||
|
|
|
@ -138,6 +138,7 @@ const reducerManageTimeline = (
|
|||
};
|
||||
|
||||
interface UseTimelineManager {
|
||||
getIndexToAddById: (id: string) => string[] | null;
|
||||
getManageTimelineById: (id: string) => ManageTimeline;
|
||||
getTimelineFilterManager: (id: string) => FilterManager | undefined;
|
||||
initializeTimeline: (newTimeline: ManageTimelineInit) => void;
|
||||
|
@ -216,9 +217,19 @@ const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseT
|
|||
},
|
||||
[initializeTimeline, state]
|
||||
);
|
||||
const getIndexToAddById = useCallback(
|
||||
(id: string): string[] | null => {
|
||||
if (state[id] != null) {
|
||||
return state[id].indexToAdd;
|
||||
}
|
||||
return getTimelineDefaults(id).indexToAdd;
|
||||
},
|
||||
[state]
|
||||
);
|
||||
const isManagedTimeline = useCallback((id: string): boolean => state[id] != null, [state]);
|
||||
|
||||
return {
|
||||
getIndexToAddById,
|
||||
getManageTimelineById,
|
||||
getTimelineFilterManager,
|
||||
initializeTimeline,
|
||||
|
@ -231,6 +242,7 @@ const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseT
|
|||
|
||||
const init = {
|
||||
getManageTimelineById: (id: string) => getTimelineDefaults(id),
|
||||
getIndexToAddById: (id: string) => null,
|
||||
getTimelineFilterManager: () => undefined,
|
||||
setIndexToAdd: () => undefined,
|
||||
isManagedTimeline: () => false,
|
||||
|
|
|
@ -90,14 +90,17 @@ export const useTimelineTypes = ({
|
|||
);
|
||||
|
||||
const onFilterClicked = useCallback(
|
||||
(tabId) => {
|
||||
if (tabId === timelineType) {
|
||||
setTimelineTypes(null);
|
||||
} else {
|
||||
setTimelineTypes(tabId);
|
||||
}
|
||||
(tabId, tabStyle: TimelineTabsStyle) => {
|
||||
setTimelineTypes((prevTimelineTypes) => {
|
||||
if (tabId === prevTimelineTypes && tabStyle === TimelineTabsStyle.filter) {
|
||||
return null;
|
||||
} else if (prevTimelineTypes !== tabId) {
|
||||
setTimelineTypes(tabId);
|
||||
}
|
||||
return prevTimelineTypes;
|
||||
});
|
||||
},
|
||||
[timelineType, setTimelineTypes]
|
||||
[setTimelineTypes]
|
||||
);
|
||||
|
||||
const timelineTabs = useMemo(() => {
|
||||
|
@ -112,7 +115,7 @@ export const useTimelineTypes = ({
|
|||
href={tab.href}
|
||||
onClick={(ev) => {
|
||||
tab.onClick(ev);
|
||||
onFilterClicked(tab.id);
|
||||
onFilterClicked(tab.id, TimelineTabsStyle.tab);
|
||||
}}
|
||||
>
|
||||
{tab.name}
|
||||
|
@ -133,7 +136,7 @@ export const useTimelineTypes = ({
|
|||
numFilters={tab.count}
|
||||
onClick={(ev: { preventDefault: () => void }) => {
|
||||
tab.onClick(ev);
|
||||
onFilterClicked(tab.id);
|
||||
onFilterClicked(tab.id, TimelineTabsStyle.filter);
|
||||
}}
|
||||
withNext={tab.withNext}
|
||||
>
|
||||
|
|
|
@ -9,7 +9,6 @@ import React from 'react';
|
|||
import { BrowserFields, DocValueFields } from '../../../../../common/containers/source';
|
||||
import { TimelineItem, TimelineNonEcsData } from '../../../../../graphql/types';
|
||||
import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
|
||||
import { maxDelay } from '../../../../../common/lib/helpers/scheduler';
|
||||
import { Note } from '../../../../../common/lib/note';
|
||||
import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers';
|
||||
import {
|
||||
|
@ -81,12 +80,13 @@ const EventsComponent: React.FC<Props> = ({
|
|||
<EventsTbody data-test-subj="events">
|
||||
{data.map((event, i) => (
|
||||
<StatefulEvent
|
||||
containerElementRef={containerElementRef}
|
||||
actionsColumnWidth={actionsColumnWidth}
|
||||
addNoteToEvent={addNoteToEvent}
|
||||
browserFields={browserFields}
|
||||
columnHeaders={columnHeaders}
|
||||
columnRenderers={columnRenderers}
|
||||
containerElementRef={containerElementRef}
|
||||
disableSensorVisibility={data != null && data.length < 101}
|
||||
docValueFields={docValueFields}
|
||||
event={event}
|
||||
eventIdToNoteIds={eventIdToNoteIds}
|
||||
|
@ -95,7 +95,6 @@ const EventsComponent: React.FC<Props> = ({
|
|||
isEventViewer={isEventViewer}
|
||||
key={`${event._id}_${event._index}`}
|
||||
loadingEventIds={loadingEventIds}
|
||||
maxDelay={maxDelay(i)}
|
||||
onColumnResized={onColumnResized}
|
||||
onPinEvent={onPinEvent}
|
||||
onRowSelected={onRowSelected}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import React, { useRef, useState, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import uuid from 'uuid';
|
||||
import VisibilitySensor from 'react-visibility-sensor';
|
||||
|
@ -12,7 +12,6 @@ import VisibilitySensor from 'react-visibility-sensor';
|
|||
import { BrowserFields, DocValueFields } from '../../../../../common/containers/source';
|
||||
import { TimelineDetailsQuery } from '../../../../containers/details';
|
||||
import { TimelineItem, DetailItem, TimelineNonEcsData } from '../../../../../graphql/types';
|
||||
import { requestIdleCallbackViaScheduler } from '../../../../../common/lib/helpers/scheduler';
|
||||
import { Note } from '../../../../../common/lib/note';
|
||||
import { ColumnHeaderOptions, TimelineModel } from '../../../../../timelines/store/timeline/model';
|
||||
import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers';
|
||||
|
@ -43,13 +42,13 @@ interface Props {
|
|||
browserFields: BrowserFields;
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
columnRenderers: ColumnRenderer[];
|
||||
disableSensorVisibility: boolean;
|
||||
docValueFields: DocValueFields[];
|
||||
event: TimelineItem;
|
||||
eventIdToNoteIds: Readonly<Record<string, string[]>>;
|
||||
getNotesByIds: (noteIds: string[]) => Note[];
|
||||
isEventViewer?: boolean;
|
||||
loadingEventIds: Readonly<string[]>;
|
||||
maxDelay?: number;
|
||||
onColumnResized: OnColumnResized;
|
||||
onPinEvent: OnPinEvent;
|
||||
onRowSelected: OnRowSelected;
|
||||
|
@ -109,6 +108,7 @@ const StatefulEventComponent: React.FC<Props> = ({
|
|||
containerElementRef,
|
||||
columnHeaders,
|
||||
columnRenderers,
|
||||
disableSensorVisibility = true,
|
||||
docValueFields,
|
||||
event,
|
||||
eventIdToNoteIds,
|
||||
|
@ -116,7 +116,6 @@ const StatefulEventComponent: React.FC<Props> = ({
|
|||
isEventViewer = false,
|
||||
isEventPinned = false,
|
||||
loadingEventIds,
|
||||
maxDelay = 0,
|
||||
onColumnResized,
|
||||
onPinEvent,
|
||||
onRowSelected,
|
||||
|
@ -130,7 +129,6 @@ const StatefulEventComponent: React.FC<Props> = ({
|
|||
updateNote,
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState<{ [eventId: string]: boolean }>({});
|
||||
const [initialRender, setInitialRender] = useState(false);
|
||||
const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({});
|
||||
const timeline = useSelector<StoreState, TimelineModel>((state) => {
|
||||
return state.timeline.timelineById['timeline-1'];
|
||||
|
@ -160,39 +158,9 @@ const StatefulEventComponent: React.FC<Props> = ({
|
|||
[addNoteToEvent, event, isEventPinned, onPinEvent]
|
||||
);
|
||||
|
||||
/**
|
||||
* Incrementally loads the events when it mounts by trying to
|
||||
* see if it resides within a window frame and if it is it will
|
||||
* indicate to React that it should render its self by setting
|
||||
* its initialRender to true.
|
||||
*/
|
||||
useEffect(() => {
|
||||
let _isMounted = true;
|
||||
|
||||
requestIdleCallbackViaScheduler(
|
||||
() => {
|
||||
if (!initialRender && _isMounted) {
|
||||
setInitialRender(true);
|
||||
}
|
||||
},
|
||||
{ timeout: maxDelay }
|
||||
);
|
||||
return () => {
|
||||
_isMounted = false;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Number of current columns plus one for actions.
|
||||
const columnCount = columnHeaders.length + 1;
|
||||
|
||||
// If we are not ready to render yet, just return null
|
||||
// see useEffect() for when it schedules the first
|
||||
// time this stateful component should be rendered.
|
||||
if (!initialRender) {
|
||||
return <SkeletonRow cellCount={columnCount} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<VisibilitySensor
|
||||
partialVisibility={true}
|
||||
|
@ -201,7 +169,7 @@ const StatefulEventComponent: React.FC<Props> = ({
|
|||
offset={{ top: TOP_OFFSET, bottom: BOTTOM_OFFSET }}
|
||||
>
|
||||
{({ isVisible }) => {
|
||||
if (isVisible) {
|
||||
if (isVisible || disableSensorVisibility) {
|
||||
return (
|
||||
<TimelineDetailsQuery
|
||||
docValueFields={docValueFields}
|
||||
|
@ -293,8 +261,8 @@ const StatefulEventComponent: React.FC<Props> = ({
|
|||
} else {
|
||||
// Height place holder for visibility detection as well as re-rendering sections.
|
||||
const height =
|
||||
divElement.current != null && divElement.current.clientHeight
|
||||
? `${divElement.current.clientHeight}px`
|
||||
divElement.current != null && divElement.current!.clientHeight
|
||||
? `${divElement.current!.clientHeight}px`
|
||||
: DEFAULT_ROW_HEIGHT;
|
||||
|
||||
return <SkeletonRow cellCount={columnCount} rowHeight={height} />;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ReactWrapper } from '@elastic/eui/node_modules/@types/enzyme';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
|
@ -18,7 +18,7 @@ import { Sort } from './sort';
|
|||
import { wait } from '../../../../common/lib/helpers';
|
||||
import { useMountAppended } from '../../../../common/utils/use_mount_appended';
|
||||
import { SELECTOR_TIMELINE_BODY_CLASS_NAME, TimelineBody } from '../styles';
|
||||
import { ReactWrapper } from '@elastic/eui/node_modules/@types/enzyme';
|
||||
import { TimelineType } from '../../../../../common/types/timeline';
|
||||
|
||||
const testBodyHeight = 700;
|
||||
const mockGetNotesByIds = (eventId: string[]) => [];
|
||||
|
@ -83,6 +83,7 @@ describe('Body', () => {
|
|||
show: true,
|
||||
sort: mockSort,
|
||||
showCheckboxes: false,
|
||||
timelineType: TimelineType.default,
|
||||
toggleColumn: jest.fn(),
|
||||
updateNote: jest.fn(),
|
||||
};
|
||||
|
|
|
@ -33,6 +33,7 @@ import { useManageTimeline } from '../../manage_timeline';
|
|||
import { GraphOverlay } from '../../graph_overlay';
|
||||
import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers';
|
||||
import { TimelineRowAction } from './actions';
|
||||
import { TimelineType } from '../../../../../common/types/timeline';
|
||||
|
||||
export interface BodyProps {
|
||||
addNoteToEvent: AddNoteToEvent;
|
||||
|
@ -64,6 +65,7 @@ export interface BodyProps {
|
|||
show: boolean;
|
||||
showCheckboxes: boolean;
|
||||
sort: Sort;
|
||||
timelineType: TimelineType;
|
||||
toggleColumn: (column: ColumnHeaderOptions) => void;
|
||||
updateNote: UpdateNote;
|
||||
}
|
||||
|
@ -101,6 +103,7 @@ export const Body = React.memo<BodyProps>(
|
|||
showCheckboxes,
|
||||
sort,
|
||||
toggleColumn,
|
||||
timelineType,
|
||||
updateNote,
|
||||
}) => {
|
||||
const containerElementRef = useRef<HTMLDivElement>(null);
|
||||
|
@ -148,7 +151,12 @@ export const Body = React.memo<BodyProps>(
|
|||
return (
|
||||
<>
|
||||
{graphEventId && (
|
||||
<GraphOverlay bodyHeight={height} graphEventId={graphEventId} timelineId={id} />
|
||||
<GraphOverlay
|
||||
bodyHeight={height}
|
||||
graphEventId={graphEventId}
|
||||
timelineId={id}
|
||||
timelineType={timelineType}
|
||||
/>
|
||||
)}
|
||||
<TimelineBody
|
||||
data-test-subj="timeline-body"
|
||||
|
|
|
@ -79,6 +79,7 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
|
|||
showCheckboxes,
|
||||
graphEventId,
|
||||
sort,
|
||||
timelineType,
|
||||
toggleColumn,
|
||||
unPinEvent,
|
||||
updateColumns,
|
||||
|
@ -218,6 +219,7 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
|
|||
show={id === TimelineId.active ? show : true}
|
||||
showCheckboxes={showCheckboxes}
|
||||
sort={sort}
|
||||
timelineType={timelineType}
|
||||
toggleColumn={toggleColumn}
|
||||
updateNote={onUpdateNote}
|
||||
/>
|
||||
|
@ -241,7 +243,8 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
|
|||
prevProps.show === nextProps.show &&
|
||||
prevProps.selectedEventIds === nextProps.selectedEventIds &&
|
||||
prevProps.showCheckboxes === nextProps.showCheckboxes &&
|
||||
prevProps.sort === nextProps.sort
|
||||
prevProps.sort === nextProps.sort &&
|
||||
prevProps.timelineType === nextProps.timelineType
|
||||
);
|
||||
|
||||
StatefulBodyComponent.displayName = 'StatefulBodyComponent';
|
||||
|
@ -268,6 +271,7 @@ const makeMapStateToProps = () => {
|
|||
selectedEventIds,
|
||||
show,
|
||||
showCheckboxes,
|
||||
timelineType,
|
||||
} = timeline;
|
||||
|
||||
return {
|
||||
|
@ -284,6 +288,7 @@ const makeMapStateToProps = () => {
|
|||
selectedEventIds,
|
||||
show,
|
||||
showCheckboxes,
|
||||
timelineType,
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
|
|
|
@ -215,6 +215,7 @@ const StatefulTimelineComponent = React.memo<Props>(
|
|||
/>
|
||||
);
|
||||
},
|
||||
// eslint-disable-next-line complexity
|
||||
(prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.eventType === nextProps.eventType &&
|
||||
|
@ -223,6 +224,7 @@ const StatefulTimelineComponent = React.memo<Props>(
|
|||
prevProps.id === nextProps.id &&
|
||||
prevProps.isLive === nextProps.isLive &&
|
||||
prevProps.isSaving === nextProps.isSaving &&
|
||||
prevProps.isTimelineExists === nextProps.isTimelineExists &&
|
||||
prevProps.itemsPerPage === nextProps.itemsPerPage &&
|
||||
prevProps.kqlMode === nextProps.kqlMode &&
|
||||
prevProps.kqlQueryExpression === nextProps.kqlQueryExpression &&
|
||||
|
|
|
@ -25,7 +25,7 @@ import { timelineSelectors } from '../../../store/timeline';
|
|||
import { setInsertTimeline } from '../../../store/timeline/actions';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { APP_ID } from '../../../../../common/constants';
|
||||
import { getCaseDetailsUrl } from '../../../../common/components/link_to';
|
||||
import { getCaseDetailsUrl, getCreateCaseUrl } from '../../../../common/components/link_to';
|
||||
|
||||
type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void;
|
||||
type UpdateTitle = ({ id, title }: { id: string; title: string }) => void;
|
||||
|
@ -111,11 +111,11 @@ export const Properties = React.memo<Props>(
|
|||
);
|
||||
|
||||
const onRowClick = useCallback(
|
||||
(id: string) => {
|
||||
(id?: string) => {
|
||||
onCloseCaseModal();
|
||||
|
||||
navigateToApp(`${APP_ID}:${SecurityPageName.case}`, {
|
||||
path: getCaseDetailsUrl({ id }),
|
||||
path: id != null ? getCaseDetailsUrl({ id }) : getCreateCaseUrl(),
|
||||
}).then(() =>
|
||||
dispatch(
|
||||
setInsertTimeline({
|
||||
|
|
Loading…
Reference in a new issue