[Security Solution] Fix flow/draggable in details event (#86834)
* fix details event * fix types + add unit test * review with angela * fix lint error Co-authored-by: Angela Chuang <yi-chun.chuang@elastic.co>
This commit is contained in:
parent
0ffb9e72ed
commit
deae756756
|
@ -408,12 +408,23 @@ export type ImportTimelineResultSchema = runtimeTypes.TypeOf<typeof importTimeli
|
|||
|
||||
export type TimelineEventsType = 'all' | 'raw' | 'alert' | 'signal' | 'custom';
|
||||
|
||||
export interface TimelineExpandedEventType {
|
||||
eventId: string;
|
||||
indexName: string;
|
||||
export enum TimelineTabs {
|
||||
query = 'query',
|
||||
graph = 'graph',
|
||||
notes = 'notes',
|
||||
pinned = 'pinned',
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type EmptyObject = Record<any, never>;
|
||||
|
||||
export type TimelineExpandedEvent = TimelineExpandedEventType | EmptyObject;
|
||||
export type TimelineExpandedEventType =
|
||||
| {
|
||||
eventId: string;
|
||||
indexName: string;
|
||||
}
|
||||
| EmptyObject;
|
||||
|
||||
export type TimelineExpandedEvent = {
|
||||
[tab in TimelineTabs]?: TimelineExpandedEventType;
|
||||
};
|
||||
|
|
|
@ -577,6 +577,7 @@ exports[`EventDetails rendering should match snapshot 1`] = `
|
|||
}
|
||||
eventId="Y-6TfmcB0WOhS6qyMv3s"
|
||||
timelineId="test"
|
||||
timelineTabType="query"
|
||||
/>
|
||||
</React.Fragment>,
|
||||
"id": "table-view",
|
||||
|
@ -1157,6 +1158,7 @@ exports[`EventDetails rendering should match snapshot 1`] = `
|
|||
}
|
||||
eventId="Y-6TfmcB0WOhS6qyMv3s"
|
||||
timelineId="test"
|
||||
timelineTabType="query"
|
||||
/>
|
||||
</React.Fragment>,
|
||||
"id": "table-view",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { waitFor } from '@testing-library/dom';
|
||||
import { ReactWrapper, shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
|
@ -16,7 +17,7 @@ import { mockBrowserFields } from '../../containers/source/mock';
|
|||
import { useMountAppended } from '../../utils/use_mount_appended';
|
||||
import { mockAlertDetailsData } from './__mocks__';
|
||||
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
|
||||
import { waitFor } from '@testing-library/dom';
|
||||
import { TimelineTabs } from '../../../../common/types/timeline';
|
||||
|
||||
jest.mock('../link_to');
|
||||
describe('EventDetails', () => {
|
||||
|
@ -27,6 +28,7 @@ describe('EventDetails', () => {
|
|||
id: mockDetailItemDataId,
|
||||
isAlert: false,
|
||||
onViewSelected: jest.fn(),
|
||||
timelineTabType: TimelineTabs.query,
|
||||
timelineId: 'test',
|
||||
view: EventsViewType.summaryView,
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@ import { EventFieldsBrowser } from './event_fields_browser';
|
|||
import { JsonView } from './json_view';
|
||||
import * as i18n from './translations';
|
||||
import { SummaryView } from './summary_view';
|
||||
import { TimelineTabs } from '../../../../common/types/timeline';
|
||||
|
||||
export type View = EventsViewType.tableView | EventsViewType.jsonView | EventsViewType.summaryView;
|
||||
export enum EventsViewType {
|
||||
|
@ -29,6 +30,7 @@ interface Props {
|
|||
isAlert: boolean;
|
||||
view: EventsViewType;
|
||||
onViewSelected: (selected: EventsViewType) => void;
|
||||
timelineTabType: TimelineTabs | 'flyout';
|
||||
timelineId: string;
|
||||
}
|
||||
|
||||
|
@ -52,6 +54,7 @@ const EventDetailsComponent: React.FC<Props> = ({
|
|||
id,
|
||||
view,
|
||||
onViewSelected,
|
||||
timelineTabType,
|
||||
timelineId,
|
||||
isAlert,
|
||||
}) => {
|
||||
|
@ -91,6 +94,7 @@ const EventDetailsComponent: React.FC<Props> = ({
|
|||
data={data}
|
||||
eventId={id}
|
||||
timelineId={timelineId}
|
||||
timelineTabType={timelineTabType}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
|
@ -106,7 +110,7 @@ const EventDetailsComponent: React.FC<Props> = ({
|
|||
),
|
||||
},
|
||||
],
|
||||
[alerts, browserFields, data, id, isAlert, timelineId]
|
||||
[alerts, browserFields, data, id, isAlert, timelineId, timelineTabType]
|
||||
);
|
||||
|
||||
const selectedTab = useMemo(() => tabs.find((t) => t.id === view) ?? tabs[0], [tabs, view]);
|
||||
|
|
|
@ -13,6 +13,7 @@ import { timelineActions } from '../../../timelines/store/timeline';
|
|||
import { EventFieldsBrowser } from './event_fields_browser';
|
||||
import { mockBrowserFields } from '../../containers/source/mock';
|
||||
import { useMountAppended } from '../../utils/use_mount_appended';
|
||||
import { TimelineTabs } from '../../../../common/types/timeline';
|
||||
|
||||
jest.mock('@elastic/eui', () => {
|
||||
const original = jest.requireActual('@elastic/eui');
|
||||
|
@ -48,6 +49,7 @@ describe('EventFieldsBrowser', () => {
|
|||
data={mockDetailItemData}
|
||||
eventId={mockDetailItemDataId}
|
||||
timelineId="test"
|
||||
timelineTabType={TimelineTabs.query}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -66,6 +68,7 @@ describe('EventFieldsBrowser', () => {
|
|||
data={mockDetailItemData}
|
||||
eventId={mockDetailItemDataId}
|
||||
timelineId="test"
|
||||
timelineTabType={TimelineTabs.query}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -89,6 +92,7 @@ describe('EventFieldsBrowser', () => {
|
|||
data={mockDetailItemData}
|
||||
eventId={eventId}
|
||||
timelineId="test"
|
||||
timelineTabType={TimelineTabs.query}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -108,6 +112,7 @@ describe('EventFieldsBrowser', () => {
|
|||
data={mockDetailItemData}
|
||||
eventId={eventId}
|
||||
timelineId="test"
|
||||
timelineTabType={TimelineTabs.query}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -127,6 +132,7 @@ describe('EventFieldsBrowser', () => {
|
|||
data={mockDetailItemData}
|
||||
eventId={eventId}
|
||||
timelineId="test"
|
||||
timelineTabType={TimelineTabs.query}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -158,6 +164,7 @@ describe('EventFieldsBrowser', () => {
|
|||
data={mockDetailItemData}
|
||||
eventId={mockDetailItemDataId}
|
||||
timelineId="test"
|
||||
timelineTabType={TimelineTabs.query}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -182,6 +189,7 @@ describe('EventFieldsBrowser', () => {
|
|||
data={mockDetailItemData}
|
||||
eventId={mockDetailItemDataId}
|
||||
timelineId="test"
|
||||
timelineTabType={TimelineTabs.query}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -196,6 +204,7 @@ describe('EventFieldsBrowser', () => {
|
|||
data={mockDetailItemData}
|
||||
eventId={mockDetailItemDataId}
|
||||
timelineId="test"
|
||||
timelineTabType={TimelineTabs.query}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -220,6 +229,7 @@ describe('EventFieldsBrowser', () => {
|
|||
data={mockDetailItemData}
|
||||
eventId={mockDetailItemDataId}
|
||||
timelineId="test"
|
||||
timelineTabType={TimelineTabs.query}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -238,6 +248,7 @@ describe('EventFieldsBrowser', () => {
|
|||
data={mockDetailItemData}
|
||||
eventId={mockDetailItemDataId}
|
||||
timelineId="test"
|
||||
timelineTabType={TimelineTabs.query}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
|
|
@ -29,12 +29,14 @@ import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
|
|||
import { getColumns } from './columns';
|
||||
import { EVENT_FIELDS_TABLE_CLASS_NAME, onEventDetailsTabKeyPressed, search } from './helpers';
|
||||
import { useDeepEqualSelector } from '../../hooks/use_selector';
|
||||
import { TimelineTabs } from '../../../../common/types/timeline';
|
||||
|
||||
interface Props {
|
||||
browserFields: BrowserFields;
|
||||
data: TimelineEventsDetailsItem[];
|
||||
eventId: string;
|
||||
timelineId: string;
|
||||
timelineTabType: TimelineTabs | 'flyout';
|
||||
}
|
||||
|
||||
const TableWrapper = styled.div`
|
||||
|
@ -87,7 +89,7 @@ const getAriaRowindex = (timelineEventsDetailsItem: TimelineEventsDetailsItem) =
|
|||
|
||||
/** Renders a table view or JSON view of the `ECS` `data` */
|
||||
export const EventFieldsBrowser = React.memo<Props>(
|
||||
({ browserFields, data, eventId, timelineId }) => {
|
||||
({ browserFields, data, eventId, timelineTabType, timelineId }) => {
|
||||
const containerElement = useRef<HTMLDivElement | null>(null);
|
||||
const dispatch = useDispatch();
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
|
@ -156,7 +158,7 @@ export const EventFieldsBrowser = React.memo<Props>(
|
|||
columnHeaders,
|
||||
eventId,
|
||||
onUpdateColumns,
|
||||
contextId: `event-fields-browser-for-${timelineId}`,
|
||||
contextId: `event-fields-browser-for-${timelineId}-${timelineTabType}`,
|
||||
timelineId,
|
||||
toggleColumn,
|
||||
getLinkValue,
|
||||
|
@ -167,6 +169,7 @@ export const EventFieldsBrowser = React.memo<Props>(
|
|||
eventId,
|
||||
onUpdateColumns,
|
||||
timelineId,
|
||||
timelineTabType,
|
||||
toggleColumn,
|
||||
getLinkValue,
|
||||
]
|
||||
|
|
|
@ -39,7 +39,7 @@ const EventDetailsFlyoutComponent: React.FC<EventDetailsFlyoutProps> = ({
|
|||
const dispatch = useDispatch();
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const expandedEvent = useDeepEqualSelector(
|
||||
(state) => (getTimeline(state, timelineId) ?? timelineDefaults)?.expandedEvent ?? {}
|
||||
(state) => (getTimeline(state, timelineId) ?? timelineDefaults)?.expandedEvent?.query ?? {}
|
||||
);
|
||||
|
||||
const handleClearSelection = useCallback(() => {
|
||||
|
@ -75,6 +75,7 @@ const EventDetailsFlyoutComponent: React.FC<EventDetailsFlyoutProps> = ({
|
|||
isAlert={isAlert}
|
||||
loading={loading}
|
||||
timelineId={timelineId}
|
||||
timelineTabType="flyout"
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
</StyledEuiFlyout>
|
||||
|
|
|
@ -5,12 +5,13 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { waitFor, act } from '@testing-library/react';
|
||||
import useResizeObserver from 'use-resize-observer/polyfilled';
|
||||
|
||||
import '../../mock/match_media';
|
||||
import { mockIndexNames, mockIndexPattern, TestProviders } from '../../mock';
|
||||
|
||||
import { mockEventViewerResponse } from './mock';
|
||||
import { mockEventViewerResponse, mockEventViewerResponseWithEvents } from './mock';
|
||||
import { StatefulEventsViewer } from '.';
|
||||
import { EventsViewer } from './events_viewer';
|
||||
import { defaultHeaders } from './default_headers';
|
||||
|
@ -30,6 +31,15 @@ jest.mock('../../../timelines/components/graph_overlay', () => ({
|
|||
GraphOverlay: jest.fn(() => <div />),
|
||||
}));
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
return {
|
||||
...original,
|
||||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@elastic/eui', () => {
|
||||
const original = jest.requireActual('@elastic/eui');
|
||||
return {
|
||||
|
@ -50,6 +60,9 @@ const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock;
|
|||
jest.mock('use-resize-observer/polyfilled');
|
||||
mockUseResizeObserver.mockImplementation(() => ({}));
|
||||
|
||||
const mockUseTimelineEvents: jest.Mock = useTimelineEvents as jest.Mock;
|
||||
jest.mock('../../../timelines/containers');
|
||||
|
||||
const from = '2019-08-26T22:10:56.791Z';
|
||||
const to = '2019-08-27T22:10:56.794Z';
|
||||
|
||||
|
@ -108,14 +121,51 @@ describe('EventsViewer', () => {
|
|||
start: from,
|
||||
scopeId: SourcererScopeName.timeline,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
(useTimelineEvents as jest.Mock).mockReturnValue([false, mockEventViewerResponse]);
|
||||
mockUseTimelineEvents.mockReset();
|
||||
});
|
||||
beforeAll(() => {
|
||||
mockUseSourcererScope.mockImplementation(() => defaultMocks);
|
||||
});
|
||||
|
||||
describe('event details', () => {
|
||||
beforeEach(() => {
|
||||
mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponseWithEvents]);
|
||||
});
|
||||
|
||||
test('call the right reduce action to show event details', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<StatefulEventsViewer {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDispatch).toBeCalledTimes(2);
|
||||
expect(mockDispatch.mock.calls[1][0]).toEqual({
|
||||
payload: {
|
||||
event: {
|
||||
eventId: 'yb8TkHYBRgU82_bJu_rY',
|
||||
indexName: 'auditbeat-7.10.1-2020.12.18-000001',
|
||||
},
|
||||
tabType: 'query',
|
||||
timelineId: 'test-stateful-events-viewer',
|
||||
},
|
||||
type: 'x-pack/security_solution/local/timeline/TOGGLE_EXPANDED_EVENT',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
beforeEach(() => {
|
||||
mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]);
|
||||
});
|
||||
|
||||
test('it renders the "Showing..." subtitle with the expected event count', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
|
@ -160,57 +210,66 @@ describe('EventsViewer', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
describe('loading', () => {
|
||||
beforeAll(() => {
|
||||
mockUseSourcererScope.mockImplementation(() => ({ ...defaultMocks, loading: true }));
|
||||
});
|
||||
test('it does NOT render fetch index pattern is loading', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<StatefulEventsViewer {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
});
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
describe('loading', () => {
|
||||
beforeAll(() => {
|
||||
mockUseSourcererScope.mockImplementation(() => ({ ...defaultMocks, loading: true }));
|
||||
});
|
||||
beforeEach(() => {
|
||||
mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]);
|
||||
});
|
||||
|
||||
test('it does NOT render when start is empty', () => {
|
||||
testProps = {
|
||||
...testProps,
|
||||
start: '',
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<StatefulEventsViewer {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
test('it does NOT render fetch index pattern is loading', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<StatefulEventsViewer {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test('it does NOT render when end is empty', () => {
|
||||
testProps = {
|
||||
...testProps,
|
||||
end: '',
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<StatefulEventsViewer {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
test('it does NOT render when start is empty', () => {
|
||||
testProps = {
|
||||
...testProps,
|
||||
start: '',
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<StatefulEventsViewer {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test('it does NOT render when end is empty', () => {
|
||||
testProps = {
|
||||
...testProps,
|
||||
end: '',
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<StatefulEventsViewer {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('headerFilterGroup', () => {
|
||||
beforeEach(() => {
|
||||
mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]);
|
||||
});
|
||||
|
||||
test('it renders the provided headerFilterGroup', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
|
@ -284,6 +343,10 @@ describe('EventsViewer', () => {
|
|||
});
|
||||
|
||||
describe('utilityBar', () => {
|
||||
beforeEach(() => {
|
||||
mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]);
|
||||
});
|
||||
|
||||
test('it renders the provided utilityBar when Resolver is NOT showing, because graphEventId is undefined', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
|
@ -313,6 +376,10 @@ describe('EventsViewer', () => {
|
|||
});
|
||||
|
||||
describe('header inspect button', () => {
|
||||
beforeEach(() => {
|
||||
mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]);
|
||||
});
|
||||
|
||||
test('it renders the inspect button when Resolver is NOT showing, because graphEventId is undefined', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
|
|
|
@ -6,21 +6,15 @@
|
|||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React, { useEffect, useMemo, useState, useRef } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { Direction } from '../../../../common/search_strategy';
|
||||
import { BrowserFields, DocValueFields } from '../../containers/source';
|
||||
import { useTimelineEvents } from '../../../timelines/containers';
|
||||
import { timelineActions } from '../../../timelines/store/timeline';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
import {
|
||||
ColumnHeaderOptions,
|
||||
KqlMode,
|
||||
TimelineTabs,
|
||||
} from '../../../timelines/store/timeline/model';
|
||||
import { ColumnHeaderOptions, KqlMode } from '../../../timelines/store/timeline/model';
|
||||
import { HeaderSection } from '../header_section';
|
||||
import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers';
|
||||
import { Sort } from '../../../timelines/components/timeline/body/sort';
|
||||
|
@ -45,7 +39,11 @@ import { inputsModel } from '../../store';
|
|||
import { useManageTimeline } from '../../../timelines/components/manage_timeline';
|
||||
import { ExitFullScreen } from '../exit_full_screen';
|
||||
import { useGlobalFullScreen } from '../../containers/use_full_screen';
|
||||
import { TimelineExpandedEvent, TimelineId } from '../../../../common/types/timeline';
|
||||
import {
|
||||
TimelineExpandedEventType,
|
||||
TimelineId,
|
||||
TimelineTabs,
|
||||
} from '../../../../common/types/timeline';
|
||||
import { GraphOverlay } from '../../../timelines/components/graph_overlay';
|
||||
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles';
|
||||
|
||||
|
@ -114,7 +112,7 @@ interface Props {
|
|||
deletedEventIds: Readonly<string[]>;
|
||||
docValueFields: DocValueFields[];
|
||||
end: string;
|
||||
expandedEvent: TimelineExpandedEvent;
|
||||
expandedEvent: TimelineExpandedEventType;
|
||||
filters: Filter[];
|
||||
headerFilterGroup?: React.ReactNode;
|
||||
height?: number;
|
||||
|
@ -160,7 +158,6 @@ const EventsViewerComponent: React.FC<Props> = ({
|
|||
utilityBar,
|
||||
graphEventId,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { globalFullScreen } = useGlobalFullScreen();
|
||||
const columnsHeader = isEmpty(columns) ? defaultHeaders : columns;
|
||||
const kibana = useKibana();
|
||||
|
@ -191,9 +188,6 @@ const EventsViewerComponent: React.FC<Props> = ({
|
|||
[justTitle]
|
||||
);
|
||||
|
||||
const prevCombinedQueries = useRef<{
|
||||
filterQuery: string;
|
||||
} | null>(null);
|
||||
const combinedQueries = combineQueries({
|
||||
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
|
||||
dataProviders,
|
||||
|
@ -220,12 +214,6 @@ const EventsViewerComponent: React.FC<Props> = ({
|
|||
queryFields,
|
||||
]);
|
||||
|
||||
const prevSortField = useRef<
|
||||
Array<{
|
||||
field: string;
|
||||
direction: Direction;
|
||||
}>
|
||||
>([]);
|
||||
const sortField = useMemo(
|
||||
() =>
|
||||
sort.map(({ columnId, sortDirection }) => ({
|
||||
|
@ -251,17 +239,6 @@ const EventsViewerComponent: React.FC<Props> = ({
|
|||
skip: !canQueryTimeline,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!deepEqual(prevCombinedQueries.current, combinedQueries)) {
|
||||
prevCombinedQueries.current = combinedQueries;
|
||||
dispatch(timelineActions.toggleExpandedEvent({ timelineId: id }));
|
||||
}
|
||||
if (!deepEqual(prevSortField.current, sortField)) {
|
||||
prevSortField.current = sortField;
|
||||
dispatch(timelineActions.toggleExpandedEvent({ timelineId: id }));
|
||||
}
|
||||
}, [combinedQueries, dispatch, id, sortField]);
|
||||
|
||||
const totalCountMinusDeleted = useMemo(
|
||||
() => (totalCount > 0 ? totalCount - deletedEventIds.length : 0),
|
||||
[deletedEventIds.length, totalCount]
|
||||
|
|
|
@ -166,7 +166,7 @@ const makeMapStateToProps = () => {
|
|||
columns,
|
||||
dataProviders,
|
||||
deletedEventIds,
|
||||
expandedEvent,
|
||||
expandedEvent: expandedEvent?.query ?? {},
|
||||
excludedRowRendererIds,
|
||||
filters: getGlobalFiltersQuerySelector(state),
|
||||
id,
|
||||
|
|
|
@ -12,3 +12,91 @@ export const mockEventViewerResponse = {
|
|||
},
|
||||
events: [],
|
||||
};
|
||||
|
||||
export const mockEventViewerResponseWithEvents = {
|
||||
totalCount: 1,
|
||||
pageInfo: {
|
||||
activePage: 0,
|
||||
fakeTotalCount: 100,
|
||||
},
|
||||
events: [
|
||||
{
|
||||
ecs: {
|
||||
_id: 'yb8TkHYBRgU82_bJu_rY',
|
||||
timestamp: '2020-12-23T14:49:39.957Z',
|
||||
_index: 'auditbeat-7.10.1-2020.12.18-000001',
|
||||
'@timestamp': ['2020-12-23T14:49:39.957Z'],
|
||||
event: {
|
||||
module: ['system'],
|
||||
action: ['process_started'],
|
||||
category: ['process'],
|
||||
dataset: ['process'],
|
||||
kind: ['event'],
|
||||
type: ['start'],
|
||||
},
|
||||
host: {
|
||||
name: ['handsome'],
|
||||
os: {
|
||||
family: ['darwin'],
|
||||
},
|
||||
id: ['33'],
|
||||
ip: ['0.0.0.0'],
|
||||
},
|
||||
user: {
|
||||
name: ['handsome'],
|
||||
},
|
||||
message: ['Process node (PID: 77895) by user handsome STARTED'],
|
||||
agent: {
|
||||
type: ['auditbeat'],
|
||||
},
|
||||
process: {
|
||||
hash: {
|
||||
sha1: ['`12345678987654323456Y7U87654`'],
|
||||
},
|
||||
pid: ['77895'],
|
||||
name: ['node'],
|
||||
ppid: ['73537'],
|
||||
args: [
|
||||
'/Users/handsome/.nvm/versions/node/v14.15.3/bin/node',
|
||||
'/Users/handsome/Documents/workspace/kibana/node_modules/jest-worker/build/workers/processChild.js',
|
||||
],
|
||||
entity_id: ['3arNfOyR9NwR2u03'],
|
||||
executable: ['/Users/handsome/.nvm/versions/node/v14.15.3/bin/node'],
|
||||
working_directory: ['/Users/handsome/Documents/workspace/kibana/x-pack'],
|
||||
},
|
||||
},
|
||||
data: [
|
||||
{
|
||||
field: '@timestamp',
|
||||
value: ['2020-12-23T14:49:39.957Z'],
|
||||
},
|
||||
{
|
||||
field: 'event.module',
|
||||
value: ['system'],
|
||||
},
|
||||
{
|
||||
field: 'event.action',
|
||||
value: ['process_started'],
|
||||
},
|
||||
{
|
||||
field: 'host.name',
|
||||
value: ['handsome'],
|
||||
},
|
||||
{
|
||||
field: 'user.name',
|
||||
value: ['handsome'],
|
||||
},
|
||||
{
|
||||
field: 'message',
|
||||
value: ['Process node (PID: 77895) by user handsome STARTED'],
|
||||
},
|
||||
{
|
||||
field: 'event.dataset',
|
||||
value: ['process'],
|
||||
},
|
||||
],
|
||||
_id: 'yb8TkHYBRgU82_bJu_rY',
|
||||
_index: 'auditbeat-7.10.1-2020.12.18-000001',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@ import { HostsTableType } from '../../../../hosts/store/model';
|
|||
import { RouteSpyState, SiemRouteType } from '../../../utils/route/types';
|
||||
import { TabNavigationProps } from '../tab_navigation/types';
|
||||
import { NetworkRouteType } from '../../../../network/pages/navigation/types';
|
||||
import { TimelineTabs } from '../../../../timelines/store/timeline/model';
|
||||
import { TimelineTabs } from '../../../../../common/types/timeline';
|
||||
|
||||
const setBreadcrumbsMock = jest.fn();
|
||||
const chromeMock = {
|
||||
|
|
|
@ -14,7 +14,7 @@ import { navTabs } from '../../../app/home/home_navigations';
|
|||
import { HostsTableType } from '../../../hosts/store/model';
|
||||
import { RouteSpyState } from '../../utils/route/types';
|
||||
import { SiemNavigationProps, SiemNavigationComponentProps } from './types';
|
||||
import { TimelineTabs } from '../../../timelines/store/timeline/model';
|
||||
import { TimelineTabs } from '../../../../common/types/timeline';
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const original = jest.requireActual('react-router-dom');
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
|
||||
import { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { TimelineTabs } from '../../../../../common/types/timeline';
|
||||
|
||||
import { navTabs } from '../../../../app/home/home_navigations';
|
||||
import { SecurityPageName } from '../../../../app/types';
|
||||
import { navTabsHostDetails } from '../../../../hosts/pages/details/nav_tabs';
|
||||
import { HostsTableType } from '../../../../hosts/store/model';
|
||||
import { TimelineTabs } from '../../../../timelines/store/timeline/model';
|
||||
import { RouteSpyState } from '../../../utils/route/types';
|
||||
import { CONSTANTS } from '../../url_state/constants';
|
||||
import { TabNavigationComponent } from './';
|
||||
|
|
|
@ -12,11 +12,11 @@ import * as H from 'history';
|
|||
import { Query, Filter } from '../../../../../../../src/plugins/data/public';
|
||||
import { url } from '../../../../../../../src/plugins/kibana_utils/public';
|
||||
|
||||
import { TimelineId } from '../../../../common/types/timeline';
|
||||
import { TimelineId, TimelineTabs } from '../../../../common/types/timeline';
|
||||
import { SecurityPageName } from '../../../app/types';
|
||||
import { inputsSelectors, State } from '../../store';
|
||||
import { UrlInputsModel } from '../../store/inputs/model';
|
||||
import { TimelineTabs, TimelineUrl } from '../../../timelines/store/timeline/model';
|
||||
import { TimelineUrl } from '../../../timelines/store/timeline/model';
|
||||
import { timelineSelectors } from '../../../timelines/store/timeline';
|
||||
import { formatDate } from '../super_date_picker';
|
||||
import { NavTab } from '../navigation/types';
|
||||
|
|
|
@ -17,7 +17,7 @@ import { Query } from '../../../../../../../src/plugins/data/public';
|
|||
import { networkModel } from '../../../network/store';
|
||||
import { hostsModel } from '../../../hosts/store';
|
||||
import { HostsTableType } from '../../../hosts/store/model';
|
||||
import { TimelineTabs } from '../../../timelines/store/timeline/model';
|
||||
import { TimelineTabs } from '../../../../common/types/timeline';
|
||||
|
||||
type Action = 'PUSH' | 'POP' | 'REPLACE';
|
||||
const pop: Action = 'POP';
|
||||
|
|
|
@ -24,13 +24,12 @@ import {
|
|||
DEFAULT_INDEX_PATTERN,
|
||||
} from '../../../common/constants';
|
||||
import { networkModel } from '../../network/store';
|
||||
import { TimelineType, TimelineStatus } from '../../../common/types/timeline';
|
||||
import { TimelineType, TimelineStatus, TimelineTabs } from '../../../common/types/timeline';
|
||||
import { mockManagementState } from '../../management/store/reducer';
|
||||
import { ManagementState } from '../../management/types';
|
||||
import { initialSourcererState, SourcererScopeName } from '../store/sourcerer/model';
|
||||
import { mockBrowserFields, mockDocValueFields } from '../containers/source/mock';
|
||||
import { mockIndexPattern } from './index_pattern';
|
||||
import { TimelineTabs } from '../../timelines/store/timeline/model';
|
||||
|
||||
export const mockGlobalState: State = {
|
||||
app: {
|
||||
|
|
|
@ -5,14 +5,19 @@
|
|||
*/
|
||||
import { FilterStateStore } from '../../../../../../src/plugins/data/common/es_query/filters/meta_filter';
|
||||
|
||||
import { TimelineId, TimelineType, TimelineStatus } from '../../../common/types/timeline';
|
||||
import {
|
||||
TimelineId,
|
||||
TimelineType,
|
||||
TimelineStatus,
|
||||
TimelineTabs,
|
||||
} from '../../../common/types/timeline';
|
||||
|
||||
import { OpenTimelineResult } from '../../timelines/components/open_timeline/types';
|
||||
import { GetAllTimeline, SortFieldTimeline, TimelineResult, Direction } from '../../graphql/types';
|
||||
import { TimelineEventsDetailsItem } from '../../../common/search_strategy';
|
||||
import { allTimelinesQuery } from '../../timelines/containers/all/index.gql_query';
|
||||
import { CreateTimelineProps } from '../../detections/components/alerts_table/types';
|
||||
import { TimelineModel, TimelineTabs } from '../../timelines/store/timeline/model';
|
||||
import { TimelineModel } from '../../timelines/store/timeline/model';
|
||||
import { timelineDefaults } from '../../timelines/store/timeline/defaults';
|
||||
|
||||
export interface MockedProvidedQuery {
|
||||
|
|
|
@ -19,10 +19,14 @@ import {
|
|||
} from '../../../common/mock/';
|
||||
import { CreateTimeline, UpdateTimelineLoading } from './types';
|
||||
import { Ecs } from '../../../../common/ecs';
|
||||
import { TimelineId, TimelineType, TimelineStatus } from '../../../../common/types/timeline';
|
||||
import {
|
||||
TimelineId,
|
||||
TimelineType,
|
||||
TimelineStatus,
|
||||
TimelineTabs,
|
||||
} from '../../../../common/types/timeline';
|
||||
import { ISearchStart } from '../../../../../../../src/plugins/data/public';
|
||||
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
|
||||
import { TimelineTabs } from '../../../timelines/store/timeline/model';
|
||||
|
||||
jest.mock('apollo-client');
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import { mount } from 'enzyme';
|
|||
import React from 'react';
|
||||
|
||||
import { TestProviders } from '../../../../common/mock/test_providers';
|
||||
import { TimelineTabs } from '../../../store/timeline/model';
|
||||
import { TimelineTabs } from '../../../../../common/types/timeline';
|
||||
import { FlyoutBottomBar } from '.';
|
||||
|
||||
describe('FlyoutBottomBar', () => {
|
||||
|
|
|
@ -14,7 +14,7 @@ import { DataProvider } from '../../timeline/data_providers/data_provider';
|
|||
import { flattenIntoAndGroups } from '../../timeline/data_providers/helpers';
|
||||
import { DataProviders } from '../../timeline/data_providers';
|
||||
import { FlyoutHeaderPanel } from '../header';
|
||||
import { TimelineTabs } from '../../../store/timeline/model';
|
||||
import { TimelineTabs } from '../../../../../common/types/timeline';
|
||||
|
||||
export const FLYOUT_BUTTON_CLASS_NAME = 'timeline-flyout-button';
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import styled from 'styled-components';
|
|||
import { FormattedRelative } from '@kbn/i18n/react';
|
||||
|
||||
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline';
|
||||
import { TimelineStatus, TimelineTabs, TimelineType } from '../../../../../common/types/timeline';
|
||||
import { timelineActions, timelineSelectors } from '../../../store/timeline';
|
||||
import { timelineDefaults } from '../../../../timelines/store/timeline/defaults';
|
||||
import { AddToFavoritesButton } from '../../timeline/properties/helpers';
|
||||
|
@ -33,7 +33,6 @@ import { ActiveTimelines } from './active_timelines';
|
|||
import * as i18n from './translations';
|
||||
import * as commonI18n from '../../timeline/properties/translations';
|
||||
import { getTimelineStatusByIdSelector } from './selectors';
|
||||
import { TimelineTabs } from '../../../store/timeline/model';
|
||||
|
||||
// to hide side borders
|
||||
const StyledPanel = styled(EuiPanel)`
|
||||
|
|
|
@ -11,10 +11,9 @@ import { useDispatch } from 'react-redux';
|
|||
import styled from 'styled-components';
|
||||
|
||||
import { AppLeaveHandler } from '../../../../../../../src/core/public';
|
||||
import { TimelineId, TimelineStatus } from '../../../../common/types/timeline';
|
||||
import { TimelineId, TimelineStatus, TimelineTabs } from '../../../../common/types/timeline';
|
||||
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { timelineActions } from '../../store/timeline';
|
||||
import { TimelineTabs } from '../../store/timeline/model';
|
||||
import { FlyoutBottomBar } from './bottom_bar';
|
||||
import { Pane } from './pane';
|
||||
import { getTimelineShowStatusByIdSelector } from './selectors';
|
||||
|
|
|
@ -6,9 +6,8 @@
|
|||
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { TimelineStatus } from '../../../../common/types/timeline';
|
||||
import { TimelineStatus, TimelineTabs } from '../../../../common/types/timeline';
|
||||
import { timelineSelectors } from '../../store/timeline';
|
||||
import { TimelineTabs } from '../../store/timeline/model';
|
||||
|
||||
export const getTimelineShowStatusByIdSelector = () =>
|
||||
createSelector(timelineSelectors.selectTimeline, (timeline) => ({
|
||||
|
|
|
@ -39,12 +39,16 @@ import { KueryFilterQueryKind } from '../../../common/store/model';
|
|||
import { Note } from '../../../common/lib/note';
|
||||
import moment from 'moment';
|
||||
import sinon from 'sinon';
|
||||
import { TimelineId, TimelineType, TimelineStatus } from '../../../../common/types/timeline';
|
||||
import {
|
||||
TimelineId,
|
||||
TimelineType,
|
||||
TimelineStatus,
|
||||
TimelineTabs,
|
||||
} from '../../../../common/types/timeline';
|
||||
import {
|
||||
mockTimeline as mockSelectedTimeline,
|
||||
mockTemplate as mockSelectedTemplate,
|
||||
} from './__mocks__';
|
||||
import { TimelineTabs } from '../../store/timeline/model';
|
||||
|
||||
jest.mock('../../../common/store/inputs/actions');
|
||||
jest.mock('../../../common/components/url_state/normalize_time_range.ts');
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
TimelineId,
|
||||
TimelineStatus,
|
||||
TimelineType,
|
||||
TimelineTabs,
|
||||
} from '../../../../common/types/timeline';
|
||||
|
||||
import {
|
||||
|
@ -42,11 +43,7 @@ import {
|
|||
addTimeline as dispatchAddTimeline,
|
||||
addNote as dispatchAddGlobalTimelineNote,
|
||||
} from '../../../timelines/store/timeline/actions';
|
||||
import {
|
||||
ColumnHeaderOptions,
|
||||
TimelineModel,
|
||||
TimelineTabs,
|
||||
} from '../../../timelines/store/timeline/model';
|
||||
import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model';
|
||||
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
|
||||
|
||||
import {
|
||||
|
|
|
@ -17,6 +17,7 @@ import { MarkdownRenderer } from '../../../../common/components/markdown_editor'
|
|||
import { timelineActions } from '../../../store/timeline';
|
||||
import { NOTE_CONTENT_CLASS_NAME } from '../../timeline/body/helpers';
|
||||
import * as i18n from './translations';
|
||||
import { TimelineTabs } from '../../../../../common/types/timeline';
|
||||
|
||||
export const NotePreviewsContainer = styled.section`
|
||||
padding-top: ${({ theme }) => `${theme.eui.euiSizeS}`};
|
||||
|
@ -37,6 +38,7 @@ const ToggleEventDetailsButtonComponent: React.FC<ToggleEventDetailsButtonProps>
|
|||
const handleClick = useCallback(() => {
|
||||
dispatch(
|
||||
timelineActions.toggleExpandedEvent({
|
||||
tabType: TimelineTabs.notes,
|
||||
timelineId,
|
||||
event: {
|
||||
eventId,
|
||||
|
|
|
@ -11,7 +11,8 @@ import { getOr } from 'lodash/fp';
|
|||
import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '../../../../../common/components/drag_and_drop/helpers';
|
||||
import { Ecs } from '../../../../../../common/ecs';
|
||||
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
|
||||
import { ColumnHeaderOptions, TimelineTabs } from '../../../../../timelines/store/timeline/model';
|
||||
import { TimelineTabs } from '../../../../../../common/types/timeline';
|
||||
import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
|
||||
import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers';
|
||||
import { EventsTd, EVENTS_TD_CLASS_NAME, EventsTdContent, EventsTdGroupData } from '../../styles';
|
||||
import { ColumnRenderer } from '../renderers/column_renderer';
|
||||
|
|
|
@ -11,7 +11,7 @@ import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants';
|
|||
import * as i18n from '../translations';
|
||||
|
||||
import { EventColumnView } from './event_column_view';
|
||||
import { TimelineType } from '../../../../../../common/types/timeline';
|
||||
import { TimelineTabs, TimelineType } from '../../../../../../common/types/timeline';
|
||||
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
|
||||
|
||||
jest.mock('../../../../../common/hooks/use_selector');
|
||||
|
@ -48,6 +48,7 @@ describe('EventColumnView', () => {
|
|||
selectedEventIds: {},
|
||||
showCheckboxes: false,
|
||||
showNotes: false,
|
||||
tabType: TimelineTabs.query,
|
||||
timelineId: 'timeline-test',
|
||||
toggleShowNotes: jest.fn(),
|
||||
updateNote: jest.fn(),
|
||||
|
|
|
@ -9,7 +9,7 @@ import React, { useCallback, useMemo } from 'react';
|
|||
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
|
||||
import { Ecs } from '../../../../../../common/ecs';
|
||||
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
|
||||
import { ColumnHeaderOptions, TimelineTabs } from '../../../../../timelines/store/timeline/model';
|
||||
import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
|
||||
import { OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events';
|
||||
import { EventsTrData } from '../../styles';
|
||||
import { Actions } from '../actions';
|
||||
|
@ -26,7 +26,7 @@ import { InvestigateInTimelineAction } from '../../../../../detections/component
|
|||
import { AddEventNoteAction } from '../actions/add_note_icon_item';
|
||||
import { PinEventAction } from '../actions/pin_event_action';
|
||||
import { inputsModel } from '../../../../../common/store';
|
||||
import { TimelineId } from '../../../../../../common/types/timeline';
|
||||
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
|
||||
import { timelineSelectors } from '../../../../store/timeline';
|
||||
import { timelineDefaults } from '../../../../store/timeline/defaults';
|
||||
import { AddToCaseAction } from '../../../../../cases/components/timeline_actions/add_to_case_action';
|
||||
|
|
|
@ -12,7 +12,8 @@ import {
|
|||
TimelineItem,
|
||||
TimelineNonEcsData,
|
||||
} from '../../../../../../common/search_strategy/timeline';
|
||||
import { ColumnHeaderOptions, TimelineTabs } from '../../../../../timelines/store/timeline/model';
|
||||
import { TimelineTabs } from '../../../../../../common/types/timeline';
|
||||
import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
|
||||
import { OnRowSelected } from '../../events';
|
||||
import { EventsTbody } from '../../styles';
|
||||
import { ColumnRenderer } from '../renderers/column_renderer';
|
||||
|
|
|
@ -8,13 +8,13 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
|
||||
import { TimelineId } from '../../../../../../common/types/timeline';
|
||||
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
|
||||
import { BrowserFields } from '../../../../../common/containers/source';
|
||||
import {
|
||||
TimelineItem,
|
||||
TimelineNonEcsData,
|
||||
} from '../../../../../../common/search_strategy/timeline';
|
||||
import { ColumnHeaderOptions, TimelineTabs } from '../../../../../timelines/store/timeline/model';
|
||||
import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
|
||||
import { OnPinEvent, OnRowSelected } from '../../events';
|
||||
import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers';
|
||||
import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles';
|
||||
|
@ -92,7 +92,10 @@ const StatefulEventComponent: React.FC<Props> = ({
|
|||
const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({});
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const expandedEvent = useDeepEqualSelector(
|
||||
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedEvent
|
||||
(state) =>
|
||||
(getTimeline(state, timelineId) ?? timelineDefaults).expandedEvent[
|
||||
tabType ?? TimelineTabs.query
|
||||
] ?? {}
|
||||
);
|
||||
const getNotesByIds = useMemo(() => appSelectors.notesByIdsSelector(), []);
|
||||
const notesById = useDeepEqualSelector(getNotesByIds);
|
||||
|
@ -153,6 +156,7 @@ const StatefulEventComponent: React.FC<Props> = ({
|
|||
|
||||
dispatch(
|
||||
timelineActions.toggleExpandedEvent({
|
||||
tabType,
|
||||
timelineId,
|
||||
event: {
|
||||
eventId,
|
||||
|
@ -161,10 +165,10 @@ const StatefulEventComponent: React.FC<Props> = ({
|
|||
})
|
||||
);
|
||||
|
||||
if (timelineId === TimelineId.active) {
|
||||
if (timelineId === TimelineId.active && tabType === TimelineTabs.query) {
|
||||
activeTimeline.toggleExpandedEvent({ eventId, indexName });
|
||||
}
|
||||
}, [dispatch, event._id, event._index, timelineId]);
|
||||
}, [dispatch, event._id, event._index, tabType, timelineId]);
|
||||
|
||||
const associateNote = useCallback(
|
||||
(noteId: string) => {
|
||||
|
|
|
@ -16,12 +16,11 @@ import {
|
|||
TimelineTypeLiteral,
|
||||
TimelineType,
|
||||
TimelineId,
|
||||
TimelineTabs,
|
||||
} from '../../../../../common/types/timeline';
|
||||
import { OnPinEvent, OnUnPinEvent } from '../events';
|
||||
import { ActionIconItem } from './actions/action_icon_item';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { TimelineTabs } from '../../../store/timeline/model';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const omitTypenameAndEmpty = (k: string, v: any): any | undefined =>
|
||||
|
|
|
@ -17,7 +17,7 @@ import { BodyComponent, StatefulBodyProps } from '.';
|
|||
import { Sort } from './sort';
|
||||
import { useMountAppended } from '../../../../common/utils/use_mount_appended';
|
||||
import { timelineActions } from '../../../store/timeline';
|
||||
import { TimelineTabs } from '../../../store/timeline/model';
|
||||
import { TimelineTabs } from '../../../../../common/types/timeline';
|
||||
|
||||
const mockSort: Sort[] = [
|
||||
{
|
||||
|
@ -221,4 +221,78 @@ describe('Body', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('event details', () => {
|
||||
beforeEach(() => {
|
||||
mockDispatch.mockReset();
|
||||
});
|
||||
test('call the right reduce action to show event details for query tab', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<BodyComponent {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click');
|
||||
wrapper.update();
|
||||
expect(mockDispatch).toBeCalledTimes(1);
|
||||
expect(mockDispatch.mock.calls[0][0]).toEqual({
|
||||
payload: {
|
||||
event: {
|
||||
eventId: '1',
|
||||
indexName: undefined,
|
||||
},
|
||||
tabType: 'query',
|
||||
timelineId: 'timeline-test',
|
||||
},
|
||||
type: 'x-pack/security_solution/local/timeline/TOGGLE_EXPANDED_EVENT',
|
||||
});
|
||||
});
|
||||
|
||||
test('call the right reduce action to show event details for pinned tab', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<BodyComponent {...props} tabType={TimelineTabs.pinned} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click');
|
||||
wrapper.update();
|
||||
expect(mockDispatch).toBeCalledTimes(1);
|
||||
expect(mockDispatch.mock.calls[0][0]).toEqual({
|
||||
payload: {
|
||||
event: {
|
||||
eventId: '1',
|
||||
indexName: undefined,
|
||||
},
|
||||
tabType: 'pinned',
|
||||
timelineId: 'timeline-test',
|
||||
},
|
||||
type: 'x-pack/security_solution/local/timeline/TOGGLE_EXPANDED_EVENT',
|
||||
});
|
||||
});
|
||||
|
||||
test('call the right reduce action to show event details for notes tab', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<BodyComponent {...props} tabType={TimelineTabs.notes} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click');
|
||||
wrapper.update();
|
||||
expect(mockDispatch).toBeCalledTimes(1);
|
||||
expect(mockDispatch.mock.calls[0][0]).toEqual({
|
||||
payload: {
|
||||
event: {
|
||||
eventId: '1',
|
||||
indexName: undefined,
|
||||
},
|
||||
tabType: 'notes',
|
||||
timelineId: 'timeline-test',
|
||||
},
|
||||
type: 'x-pack/security_solution/local/timeline/TOGGLE_EXPANDED_EVENT',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { RowRendererId, TimelineId } from '../../../../../common/types/timeline';
|
||||
import { RowRendererId, TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
|
||||
import {
|
||||
FIRST_ARIA_INDEX,
|
||||
ARIA_COLINDEX_ATTRIBUTE,
|
||||
|
@ -21,7 +21,7 @@ import { BrowserFields } from '../../../../common/containers/source';
|
|||
import { TimelineItem } from '../../../../../common/search_strategy/timeline';
|
||||
import { inputsModel, State } from '../../../../common/store';
|
||||
import { useManageTimeline } from '../../manage_timeline';
|
||||
import { ColumnHeaderOptions, TimelineModel, TimelineTabs } from '../../../store/timeline/model';
|
||||
import { ColumnHeaderOptions, TimelineModel } from '../../../store/timeline/model';
|
||||
import { timelineDefaults } from '../../../store/timeline/defaults';
|
||||
import { timelineActions, timelineSelectors } from '../../../store/timeline';
|
||||
import { OnRowSelected, OnSelectAll } from '../events';
|
||||
|
|
|
@ -25,10 +25,12 @@ import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
|
|||
import { useTimelineEventsDetails } from '../../containers/details';
|
||||
import { timelineSelectors } from '../../store/timeline';
|
||||
import { timelineDefaults } from '../../store/timeline/defaults';
|
||||
import { TimelineTabs } from '../../../../common/types/timeline';
|
||||
|
||||
interface EventDetailsProps {
|
||||
browserFields: BrowserFields;
|
||||
docValueFields: DocValueFields[];
|
||||
tabType: TimelineTabs;
|
||||
timelineId: string;
|
||||
handleOnEventClosed?: HandleOnEventClosed;
|
||||
}
|
||||
|
@ -36,12 +38,13 @@ interface EventDetailsProps {
|
|||
const EventDetailsComponent: React.FC<EventDetailsProps> = ({
|
||||
browserFields,
|
||||
docValueFields,
|
||||
tabType,
|
||||
timelineId,
|
||||
handleOnEventClosed,
|
||||
}) => {
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const expandedEvent = useDeepEqualSelector(
|
||||
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedEvent
|
||||
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedEvent[tabType] ?? {}
|
||||
);
|
||||
|
||||
const [loading, detailsData] = useTimelineEventsDetails({
|
||||
|
@ -71,6 +74,7 @@ const EventDetailsComponent: React.FC<EventDetailsProps> = ({
|
|||
isAlert={isAlert}
|
||||
loading={loading}
|
||||
timelineId={timelineId}
|
||||
timelineTabType={tabType}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { TimelineExpandedEvent } from '../../../../../common/types/timeline';
|
||||
import { TimelineExpandedEventType, TimelineTabs } from '../../../../../common/types/timeline';
|
||||
import { BrowserFields } from '../../../../common/containers/source';
|
||||
import {
|
||||
EventDetails,
|
||||
|
@ -35,9 +35,10 @@ export type HandleOnEventClosed = () => void;
|
|||
interface Props {
|
||||
browserFields: BrowserFields;
|
||||
detailsData: TimelineEventsDetailsItem[] | null;
|
||||
event: TimelineExpandedEvent;
|
||||
event: TimelineExpandedEventType;
|
||||
isAlert: boolean;
|
||||
loading: boolean;
|
||||
timelineTabType: TimelineTabs | 'flyout';
|
||||
timelineId: string;
|
||||
}
|
||||
|
||||
|
@ -71,7 +72,7 @@ export const ExpandableEventTitle = React.memo<ExpandableEventTitleProps>(
|
|||
ExpandableEventTitle.displayName = 'ExpandableEventTitle';
|
||||
|
||||
export const ExpandableEvent = React.memo<Props>(
|
||||
({ browserFields, event, timelineId, isAlert, loading, detailsData }) => {
|
||||
({ browserFields, event, timelineId, timelineTabType, isAlert, loading, detailsData }) => {
|
||||
const [view, setView] = useState<View>(EventsViewType.summaryView);
|
||||
|
||||
const message = useMemo(() => {
|
||||
|
@ -116,6 +117,7 @@ export const ExpandableEvent = React.memo<Props>(
|
|||
id={event.eventId!}
|
||||
isAlert={isAlert}
|
||||
onViewSelected={setView}
|
||||
timelineTabType={timelineTabType}
|
||||
timelineId={timelineId}
|
||||
view={view}
|
||||
/>
|
||||
|
|
|
@ -17,7 +17,7 @@ import { isTab } from '../../../common/components/accessibility/helpers';
|
|||
import { useSourcererScope } from '../../../common/containers/sourcerer';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { FlyoutHeader, FlyoutHeaderPanel } from '../flyout/header';
|
||||
import { TimelineType } from '../../../../common/types/timeline';
|
||||
import { TimelineType, TimelineTabs } from '../../../../common/types/timeline';
|
||||
import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { activeTimeline } from '../../containers/active_timeline_context';
|
||||
import { EVENTS_COUNT_BUTTON_CLASS_NAME, onTimelineTabKeyPressed } from './helpers';
|
||||
|
@ -68,7 +68,9 @@ const StatefulTimelineComponent: React.FC<Props> = ({ timelineId }) => {
|
|||
id: timelineId,
|
||||
columns: defaultHeaders,
|
||||
indexNames: selectedPatterns,
|
||||
expandedEvent: activeTimeline.getExpandedEvent(),
|
||||
expandedEvent: {
|
||||
[TimelineTabs.query]: activeTimeline.getExpandedEvent(),
|
||||
},
|
||||
show: false,
|
||||
})
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { filter, pick, uniqBy } from 'lodash/fp';
|
||||
import { filter, uniqBy } from 'lodash/fp';
|
||||
import {
|
||||
EuiAvatar,
|
||||
EuiFlexGroup,
|
||||
|
@ -21,17 +21,17 @@ import styled from 'styled-components';
|
|||
|
||||
import { useSourcererScope } from '../../../../common/containers/sourcerer';
|
||||
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
|
||||
import { timelineActions, timelineSelectors } from '../../../store/timeline';
|
||||
import { timelineActions } from '../../../store/timeline';
|
||||
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { TimelineStatus } from '../../../../../common/types/timeline';
|
||||
import { TimelineStatus, TimelineTabs } from '../../../../../common/types/timeline';
|
||||
import { appSelectors } from '../../../../common/store/app';
|
||||
import { timelineDefaults } from '../../../store/timeline/defaults';
|
||||
import { AddNote } from '../../notes/add_note';
|
||||
import { CREATED_BY, NOTES } from '../../notes/translations';
|
||||
import { PARTICIPANTS } from '../../../../cases/translations';
|
||||
import { NotePreviews } from '../../open_timeline/note_previews';
|
||||
import { TimelineResultNote } from '../../open_timeline/types';
|
||||
import { EventDetails } from '../event_details';
|
||||
import { getTimelineNoteSelector } from './selectors';
|
||||
|
||||
const FullWidthFlexGroup = styled(EuiFlexGroup)`
|
||||
width: 100%;
|
||||
|
@ -121,18 +121,14 @@ interface NotesTabContentProps {
|
|||
|
||||
const NotesTabContentComponent: React.FC<NotesTabContentProps> = ({ timelineId }) => {
|
||||
const dispatch = useDispatch();
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const getTimelineNotes = useMemo(() => getTimelineNoteSelector(), []);
|
||||
const {
|
||||
createdBy,
|
||||
expandedEvent,
|
||||
eventIdToNoteIds,
|
||||
noteIds,
|
||||
status: timelineStatus,
|
||||
} = useDeepEqualSelector((state) =>
|
||||
pick(
|
||||
['createdBy', 'expandedEvent', 'eventIdToNoteIds', 'status'],
|
||||
getTimeline(state, timelineId) ?? timelineDefaults
|
||||
)
|
||||
);
|
||||
} = useDeepEqualSelector((state) => getTimelineNotes(state, timelineId));
|
||||
|
||||
const { browserFields, docValueFields } = useSourcererScope(SourcererScopeName.timeline);
|
||||
|
||||
|
@ -142,7 +138,20 @@ const NotesTabContentComponent: React.FC<NotesTabContentProps> = ({ timelineId }
|
|||
);
|
||||
const [newNote, setNewNote] = useState('');
|
||||
const isImmutable = timelineStatus === TimelineStatus.immutable;
|
||||
const notes: TimelineResultNote[] = useDeepEqualSelector(getNotesAsCommentsList);
|
||||
const appNotes: TimelineResultNote[] = useDeepEqualSelector(getNotesAsCommentsList);
|
||||
|
||||
const allTimelineNoteIds = useMemo(() => {
|
||||
const eventNoteIds = Object.values(eventIdToNoteIds).reduce<string[]>(
|
||||
(acc, v) => [...acc, ...v],
|
||||
[]
|
||||
);
|
||||
return [...noteIds, ...eventNoteIds];
|
||||
}, [noteIds, eventIdToNoteIds]);
|
||||
|
||||
const notes = useMemo(
|
||||
() => appNotes.filter((appNote) => allTimelineNoteIds.includes(appNote?.noteId ?? '-1')),
|
||||
[appNotes, allTimelineNoteIds]
|
||||
);
|
||||
|
||||
// filter for savedObjectId to make sure we don't display `elastic` user while saving the note
|
||||
const participants = useMemo(() => uniqBy('updatedBy', filter('savedObjectId', notes)), [notes]);
|
||||
|
@ -153,20 +162,21 @@ const NotesTabContentComponent: React.FC<NotesTabContentProps> = ({ timelineId }
|
|||
);
|
||||
|
||||
const handleOnEventClosed = useCallback(() => {
|
||||
dispatch(timelineActions.toggleExpandedEvent({ timelineId }));
|
||||
dispatch(timelineActions.toggleExpandedEvent({ tabType: TimelineTabs.notes, timelineId }));
|
||||
}, [dispatch, timelineId]);
|
||||
|
||||
const EventDetailsContent = useMemo(
|
||||
() =>
|
||||
expandedEvent.eventId ? (
|
||||
expandedEvent?.eventId != null ? (
|
||||
<EventDetails
|
||||
browserFields={browserFields}
|
||||
docValueFields={docValueFields}
|
||||
handleOnEventClosed={handleOnEventClosed}
|
||||
tabType={TimelineTabs.notes}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
) : null,
|
||||
[browserFields, docValueFields, expandedEvent.eventId, handleOnEventClosed, timelineId]
|
||||
[browserFields, docValueFields, expandedEvent, handleOnEventClosed, timelineId]
|
||||
);
|
||||
|
||||
const SidebarContent = useMemo(
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { createSelector } from 'reselect';
|
||||
|
||||
import { timelineSelectors } from '../../../store/timeline';
|
||||
|
||||
export const getTimelineNoteSelector = () =>
|
||||
createSelector(timelineSelectors.selectTimeline, (timeline) => {
|
||||
return {
|
||||
createdBy: timeline.createdBy,
|
||||
expandedEvent: timeline.expandedEvent?.notes ?? {},
|
||||
eventIdToNoteIds: timeline?.eventIdToNoteIds ?? {},
|
||||
noteIds: timeline.noteIds,
|
||||
status: timeline.status,
|
||||
};
|
||||
});
|
|
@ -23,11 +23,12 @@ import { EventDetailsWidthProvider } from '../../../../common/components/events_
|
|||
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
|
||||
import { timelineDefaults } from '../../../store/timeline/defaults';
|
||||
import { useSourcererScope } from '../../../../common/containers/sourcerer';
|
||||
import { TimelineModel, TimelineTabs } from '../../../store/timeline/model';
|
||||
import { TimelineModel } from '../../../store/timeline/model';
|
||||
import { EventDetails } from '../event_details';
|
||||
import { ToggleExpandedEvent } from '../../../store/timeline/actions';
|
||||
import { State } from '../../../../common/store';
|
||||
import { calculateTotalPages } from '../helpers';
|
||||
import { TimelineTabs } from '../../../../../common/types/timeline';
|
||||
|
||||
const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
|
||||
overflow-y: hidden;
|
||||
|
@ -167,7 +168,7 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
|
|||
});
|
||||
|
||||
const handleOnEventClosed = useCallback(() => {
|
||||
onEventClosed({ timelineId });
|
||||
onEventClosed({ tabType: TimelineTabs.pinned, timelineId });
|
||||
}, [timelineId, onEventClosed]);
|
||||
|
||||
return (
|
||||
|
@ -218,6 +219,7 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
|
|||
<EventDetails
|
||||
browserFields={browserFields}
|
||||
docValueFields={docValueFields}
|
||||
tabType={TimelineTabs.pinned}
|
||||
timelineId={timelineId}
|
||||
handleOnEventClosed={handleOnEventClosed}
|
||||
/>
|
||||
|
@ -248,7 +250,7 @@ const makeMapStateToProps = () => {
|
|||
itemsPerPage,
|
||||
itemsPerPageOptions,
|
||||
pinnedEventIds,
|
||||
showEventDetails: !!expandedEvent.eventId,
|
||||
showEventDetails: !!expandedEvent[TimelineTabs.pinned]?.eventId,
|
||||
sort,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -17,12 +17,11 @@ import { QueryTabContentComponent, Props as QueryTabContentComponentProps } from
|
|||
import { Sort } from '../body/sort';
|
||||
import { mockDataProviders } from '../data_providers/mock/mock_data_providers';
|
||||
import { useMountAppended } from '../../../../common/utils/use_mount_appended';
|
||||
import { TimelineId, TimelineStatus } from '../../../../../common/types/timeline';
|
||||
import { TimelineId, TimelineStatus, TimelineTabs } from '../../../../../common/types/timeline';
|
||||
import { useTimelineEvents } from '../../../containers/index';
|
||||
import { useTimelineEventsDetails } from '../../../containers/details/index';
|
||||
import { useSourcererScope } from '../../../../common/containers/sourcerer';
|
||||
import { mockSourcererScope } from '../../../../common/containers/sourcerer/mocks';
|
||||
import { TimelineTabs } from '../../../store/timeline/model';
|
||||
|
||||
jest.mock('../../../containers/index', () => ({
|
||||
useTimelineEvents: jest.fn(),
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
EuiBadge,
|
||||
} from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Dispatch } from 'redux';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
@ -33,7 +33,7 @@ import { calculateTotalPages, combineQueries } from '../helpers';
|
|||
import { TimelineRefetch } from '../refetch_timeline';
|
||||
import { esQuery, FilterManager } from '../../../../../../../../src/plugins/data/public';
|
||||
import { useManageTimeline } from '../../manage_timeline';
|
||||
import { TimelineEventsType, TimelineId } from '../../../../../common/types/timeline';
|
||||
import { TimelineEventsType, TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
|
||||
import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config';
|
||||
import { SuperDatePicker } from '../../../../common/components/super_date_picker';
|
||||
import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context';
|
||||
|
@ -44,7 +44,7 @@ import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
|
|||
import { timelineDefaults } from '../../../../timelines/store/timeline/defaults';
|
||||
import { useSourcererScope } from '../../../../common/containers/sourcerer';
|
||||
import { useTimelineEventsCountPortal } from '../../../../common/hooks/use_timeline_events_count';
|
||||
import { TimelineModel, TimelineTabs } from '../../../../timelines/store/timeline/model';
|
||||
import { TimelineModel } from '../../../../timelines/store/timeline/model';
|
||||
import { EventDetails } from '../event_details';
|
||||
import { TimelineDatePickerLock } from '../date_picker_lock';
|
||||
import { HideShowContainer } from '../styles';
|
||||
|
@ -173,9 +173,6 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
kqlQueryExpression,
|
||||
]);
|
||||
|
||||
const prevCombinedQueries = useRef<{
|
||||
filterQuery: string;
|
||||
} | null>(null);
|
||||
const combinedQueries = useMemo(
|
||||
() =>
|
||||
combineQueries({
|
||||
|
@ -211,12 +208,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
|
||||
return [...columnFields, ...requiredFieldsForActions];
|
||||
}, [columns]);
|
||||
const prevTimelineQuerySortField = useRef<
|
||||
Array<{
|
||||
field: string;
|
||||
direction: Direction;
|
||||
}>
|
||||
>([]);
|
||||
|
||||
const timelineQuerySortField = useMemo(
|
||||
() =>
|
||||
sort.map(({ columnId, sortDirection }) => ({
|
||||
|
@ -252,7 +244,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
});
|
||||
|
||||
const handleOnEventClosed = useCallback(() => {
|
||||
onEventClosed({ timelineId });
|
||||
onEventClosed({ tabType: TimelineTabs.query, timelineId });
|
||||
|
||||
if (timelineId === TimelineId.active) {
|
||||
activeTimeline.toggleExpandedEvent({
|
||||
|
@ -266,17 +258,6 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
setIsTimelineLoading({ id: timelineId, isLoading: isQueryLoading || loadingSourcerer });
|
||||
}, [loadingSourcerer, timelineId, isQueryLoading, setIsTimelineLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!deepEqual(prevCombinedQueries.current, combinedQueries)) {
|
||||
prevCombinedQueries.current = combinedQueries;
|
||||
handleOnEventClosed();
|
||||
}
|
||||
if (!deepEqual(prevTimelineQuerySortField.current, timelineQuerySortField)) {
|
||||
prevTimelineQuerySortField.current = timelineQuerySortField;
|
||||
handleOnEventClosed();
|
||||
}
|
||||
}, [combinedQueries, handleOnEventClosed, timelineQuerySortField]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<InPortal node={timelineEventsCountPortalNode}>
|
||||
|
@ -368,6 +349,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
<EventDetails
|
||||
browserFields={browserFields}
|
||||
docValueFields={docValueFields}
|
||||
tabType={TimelineTabs.query}
|
||||
timelineId={timelineId}
|
||||
handleOnEventClosed={handleOnEventClosed}
|
||||
/>
|
||||
|
@ -416,7 +398,7 @@ const makeMapStateToProps = () => {
|
|||
dataProviders,
|
||||
eventType: eventType ?? 'raw',
|
||||
end: input.timerange.to,
|
||||
expandedEvent,
|
||||
expandedEvent: expandedEvent[TimelineTabs.query] ?? {},
|
||||
filters: timelineFilter,
|
||||
timelineId,
|
||||
isLive: input.policy.kind === 'interval',
|
||||
|
@ -425,7 +407,7 @@ const makeMapStateToProps = () => {
|
|||
kqlMode,
|
||||
kqlQueryExpression,
|
||||
showCallOutUnauthorizedMsg: getShowCallOutUnauthorizedMsg(state),
|
||||
showEventDetails: !!expandedEvent.eventId,
|
||||
showEventDetails: !!expandedEvent[TimelineTabs.query]?.eventId,
|
||||
show,
|
||||
sort,
|
||||
start: input.timerange.from,
|
||||
|
|
|
@ -8,16 +8,21 @@ import { EuiBadge, EuiLoadingContent, EuiTabs, EuiTab } from '@elastic/eui';
|
|||
import React, { lazy, memo, Suspense, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import { TimelineTabs } from '../../../../../common/types/timeline';
|
||||
|
||||
import { useShallowEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import {
|
||||
useShallowEqualSelector,
|
||||
useDeepEqualSelector,
|
||||
} from '../../../../common/hooks/use_selector';
|
||||
import { TimelineEventsCountBadge } from '../../../../common/hooks/use_timeline_events_count';
|
||||
import { timelineActions } from '../../../store/timeline';
|
||||
import { TimelineTabs } from '../../../store/timeline/model';
|
||||
import {
|
||||
getActiveTabSelector,
|
||||
getNoteIdsSelector,
|
||||
getNotesSelector,
|
||||
getPinnedEventSelector,
|
||||
getShowTimelineSelector,
|
||||
getEventIdToNoteIdsSelector,
|
||||
} from './selectors';
|
||||
import * as i18n from './translations';
|
||||
|
||||
|
@ -137,37 +142,55 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({ timelineId, graphEve
|
|||
const getActiveTab = useMemo(() => getActiveTabSelector(), []);
|
||||
const getShowTimeline = useMemo(() => getShowTimelineSelector(), []);
|
||||
const getNumberOfPinnedEvents = useMemo(() => getPinnedEventSelector(), []);
|
||||
const getNumberOfNotes = useMemo(() => getNotesSelector(), []);
|
||||
const getAppNotes = useMemo(() => getNotesSelector(), []);
|
||||
const getTimelineNoteIds = useMemo(() => getNoteIdsSelector(), []);
|
||||
const getTimelinePinnedEventNotes = useMemo(() => getEventIdToNoteIdsSelector(), []);
|
||||
|
||||
const activeTab = useShallowEqualSelector((state) => getActiveTab(state, timelineId));
|
||||
const showTimeline = useShallowEqualSelector((state) => getShowTimeline(state, timelineId));
|
||||
const numberOfPinnedEvents = useShallowEqualSelector((state) =>
|
||||
getNumberOfPinnedEvents(state, timelineId)
|
||||
);
|
||||
const numberOfNotes = useShallowEqualSelector((state) => getNumberOfNotes(state));
|
||||
const globalTimelineNoteIds = useDeepEqualSelector((state) =>
|
||||
getTimelineNoteIds(state, timelineId)
|
||||
);
|
||||
const eventIdToNoteIds = useDeepEqualSelector((state) =>
|
||||
getTimelinePinnedEventNotes(state, timelineId)
|
||||
);
|
||||
const appNotes = useDeepEqualSelector((state) => getAppNotes(state));
|
||||
|
||||
const allTimelineNoteIds = useMemo(() => {
|
||||
const eventNoteIds = Object.values(eventIdToNoteIds).reduce<string[]>(
|
||||
(acc, v) => [...acc, ...v],
|
||||
[]
|
||||
);
|
||||
return [...globalTimelineNoteIds, ...eventNoteIds];
|
||||
}, [globalTimelineNoteIds, eventIdToNoteIds]);
|
||||
|
||||
const numberOfNotes = useMemo(
|
||||
() => appNotes.filter((appNote) => allTimelineNoteIds.includes(appNote.id)).length,
|
||||
[appNotes, allTimelineNoteIds]
|
||||
);
|
||||
|
||||
const setQueryAsActiveTab = useCallback(() => {
|
||||
dispatch(timelineActions.toggleExpandedEvent({ timelineId }));
|
||||
dispatch(
|
||||
timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.query })
|
||||
);
|
||||
}, [dispatch, timelineId]);
|
||||
|
||||
const setGraphAsActiveTab = useCallback(() => {
|
||||
dispatch(timelineActions.toggleExpandedEvent({ timelineId }));
|
||||
dispatch(
|
||||
timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.graph })
|
||||
);
|
||||
}, [dispatch, timelineId]);
|
||||
|
||||
const setNotesAsActiveTab = useCallback(() => {
|
||||
dispatch(timelineActions.toggleExpandedEvent({ timelineId }));
|
||||
dispatch(
|
||||
timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.notes })
|
||||
);
|
||||
}, [dispatch, timelineId]);
|
||||
|
||||
const setPinnedAsActiveTab = useCallback(() => {
|
||||
dispatch(timelineActions.toggleExpandedEvent({ timelineId }));
|
||||
dispatch(
|
||||
timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.pinned })
|
||||
);
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
*/
|
||||
|
||||
import { createSelector } from 'reselect';
|
||||
import { TimelineTabs } from '../../../../../common/types/timeline';
|
||||
import { selectNotesById } from '../../../../common/store/app/selectors';
|
||||
import { TimelineTabs } from '../../../store/timeline/model';
|
||||
import { selectTimeline } from '../../../store/timeline/selectors';
|
||||
|
||||
export const getActiveTabSelector = () =>
|
||||
|
@ -18,5 +18,11 @@ export const getShowTimelineSelector = () =>
|
|||
export const getPinnedEventSelector = () =>
|
||||
createSelector(selectTimeline, (timeline) => Object.keys(timeline?.pinnedEventIds ?? {}).length);
|
||||
|
||||
export const getNoteIdsSelector = () =>
|
||||
createSelector(selectTimeline, (timeline) => timeline?.noteIds ?? []);
|
||||
|
||||
export const getEventIdToNoteIdsSelector = () =>
|
||||
createSelector(selectTimeline, (timeline) => timeline?.eventIdToNoteIds ?? {});
|
||||
|
||||
export const getNotesSelector = () =>
|
||||
createSelector(selectNotesById, (notesById) => Object.keys(notesById ?? {}).length);
|
||||
createSelector(selectNotesById, (notesById) => Object.values(notesById));
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { TimelineExpandedEvent } from '../../../common/types/timeline';
|
||||
import { TimelineExpandedEventType } from '../../../common/types/timeline';
|
||||
import { TimelineEventsAllRequestOptions } from '../../../common/search_strategy/timeline';
|
||||
import { TimelineArgs } from '.';
|
||||
|
||||
|
@ -21,7 +21,7 @@ import { TimelineArgs } from '.';
|
|||
|
||||
class ActiveTimelineEvents {
|
||||
private _activePage: number = 0;
|
||||
private _expandedEvent: TimelineExpandedEvent = {};
|
||||
private _expandedEvent: TimelineExpandedEventType = {};
|
||||
private _pageName: string = '';
|
||||
private _request: TimelineEventsAllRequestOptions | null = null;
|
||||
private _response: TimelineArgs | null = null;
|
||||
|
@ -38,7 +38,7 @@ class ActiveTimelineEvents {
|
|||
return this._expandedEvent;
|
||||
}
|
||||
|
||||
toggleExpandedEvent(expandedEvent: TimelineExpandedEvent) {
|
||||
toggleExpandedEvent(expandedEvent: TimelineExpandedEventType) {
|
||||
if (expandedEvent.eventId === this._expandedEvent.eventId) {
|
||||
this._expandedEvent = {};
|
||||
} else {
|
||||
|
@ -46,7 +46,7 @@ class ActiveTimelineEvents {
|
|||
}
|
||||
}
|
||||
|
||||
setExpandedEvent(expandedEvent: TimelineExpandedEvent) {
|
||||
setExpandedEvent(expandedEvent: TimelineExpandedEventType) {
|
||||
this._expandedEvent = expandedEvent;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { noop } from 'lodash/fp';
|
||||
import { isEmpty, noop } from 'lodash/fp';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
|
@ -18,6 +18,7 @@ import {
|
|||
TimelineEventsDetailsStrategyResponse,
|
||||
} from '../../../../common/search_strategy';
|
||||
import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/public';
|
||||
import { AbortError } from '../../../../../../../src/plugins/kibana_utils/common';
|
||||
export interface EventsArgs {
|
||||
detailsData: TimelineEventsDetailsItem[] | null;
|
||||
}
|
||||
|
@ -50,7 +51,7 @@ export const useTimelineEventsDetails = ({
|
|||
|
||||
const timelineDetailsSearch = useCallback(
|
||||
(request: TimelineEventsDetailsRequestOptions | null) => {
|
||||
if (request == null || skip) {
|
||||
if (request == null || skip || isEmpty(request.eventId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -84,11 +85,13 @@ export const useTimelineEventsDetails = ({
|
|||
searchSubscription$.unsubscribe();
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
error: (msg) => {
|
||||
if (!didCancel) {
|
||||
setLoading(false);
|
||||
}
|
||||
notifications.toasts.addDanger('Failed to run search');
|
||||
if (!(msg instanceof AbortError)) {
|
||||
notifications.toasts.addDanger('Failed to run search');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -15,13 +15,15 @@ import {
|
|||
} from '../../../timelines/components/timeline/data_providers/data_provider';
|
||||
import { SerializedFilterQuery } from '../../../common/store/types';
|
||||
|
||||
import { KqlMode, TimelineModel, ColumnHeaderOptions, TimelineTabs } from './model';
|
||||
import { KqlMode, TimelineModel, ColumnHeaderOptions } from './model';
|
||||
import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline';
|
||||
import {
|
||||
TimelineEventsType,
|
||||
TimelineExpandedEvent,
|
||||
TimelineExpandedEventType,
|
||||
TimelineTypeLiteral,
|
||||
RowRendererId,
|
||||
TimelineExpandedEvent,
|
||||
TimelineTabs,
|
||||
} from '../../../../common/types/timeline';
|
||||
import { InsertTimeline } from './types';
|
||||
|
||||
|
@ -36,8 +38,9 @@ export const addNoteToEvent = actionCreator<{ id: string; noteId: string; eventI
|
|||
);
|
||||
|
||||
export interface ToggleExpandedEvent {
|
||||
event?: TimelineExpandedEventType;
|
||||
tabType?: TimelineTabs;
|
||||
timelineId: string;
|
||||
event?: TimelineExpandedEvent;
|
||||
}
|
||||
export const toggleExpandedEvent = actionCreator<ToggleExpandedEvent>('TOGGLE_EXPANDED_EVENT');
|
||||
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { TimelineType, TimelineStatus } from '../../../../common/types/timeline';
|
||||
import { TimelineType, TimelineStatus, TimelineTabs } from '../../../../common/types/timeline';
|
||||
|
||||
import { Direction } from '../../../graphql/types';
|
||||
import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers';
|
||||
import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range';
|
||||
import { SubsetTimelineModel, TimelineModel, TimelineTabs } from './model';
|
||||
import { SubsetTimelineModel, TimelineModel } from './model';
|
||||
|
||||
// normalizeTimeRange uses getTimeRangeSettings which cannot be used outside Kibana context if the uiSettings is not false
|
||||
const { from: start, to: end } = normalizeTimeRange({ from: '', to: '' }, false);
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
*/
|
||||
|
||||
import { Filter, esFilters } from '../../../../../../../src/plugins/data/public';
|
||||
import { TimelineType, TimelineStatus } from '../../../../common/types/timeline';
|
||||
import { TimelineType, TimelineStatus, TimelineTabs } from '../../../../common/types/timeline';
|
||||
import { Direction } from '../../../graphql/types';
|
||||
import { convertTimelineAsInput } from './epic';
|
||||
import { TimelineModel, TimelineTabs } from './model';
|
||||
import { TimelineModel } from './model';
|
||||
|
||||
describe('Epic Timeline', () => {
|
||||
describe('#convertTimelineAsInput ', () => {
|
||||
|
|
|
@ -40,8 +40,7 @@ import { Direction } from '../../../graphql/types';
|
|||
|
||||
import { addTimelineInStorage } from '../../containers/local_storage';
|
||||
import { isPageTimeline } from './epic_local_storage';
|
||||
import { TimelineId, TimelineStatus } from '../../../../common/types/timeline';
|
||||
import { TimelineTabs } from './model';
|
||||
import { TimelineId, TimelineStatus, TimelineTabs } from '../../../../common/types/timeline';
|
||||
|
||||
jest.mock('../../containers/local_storage');
|
||||
|
||||
|
|
|
@ -17,11 +17,11 @@ import type {
|
|||
TimelineType,
|
||||
TimelineStatus,
|
||||
RowRendererId,
|
||||
TimelineTabs,
|
||||
} from '../../../../common/types/timeline';
|
||||
|
||||
export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages
|
||||
export type KqlMode = 'filter' | 'search';
|
||||
|
||||
export type ColumnHeaderType = 'not-filtered' | 'text-filter';
|
||||
|
||||
/** Uniquely identifies a column */
|
||||
|
@ -43,13 +43,6 @@ export interface ColumnHeaderOptions {
|
|||
width: number;
|
||||
}
|
||||
|
||||
export enum TimelineTabs {
|
||||
query = 'query',
|
||||
graph = 'graph',
|
||||
notes = 'notes',
|
||||
pinned = 'pinned',
|
||||
}
|
||||
|
||||
export interface TimelineModel {
|
||||
/** The selected tab to displayed in the timeline */
|
||||
activeTab: TimelineTabs;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { TimelineType, TimelineStatus } from '../../../../common/types/timeline';
|
||||
import { TimelineType, TimelineStatus, TimelineTabs } from '../../../../common/types/timeline';
|
||||
|
||||
import {
|
||||
IS_OPERATOR,
|
||||
|
@ -40,7 +40,7 @@ import {
|
|||
updateTimelineTitle,
|
||||
upsertTimelineColumn,
|
||||
} from './helpers';
|
||||
import { ColumnHeaderOptions, TimelineModel, TimelineTabs } from './model';
|
||||
import { ColumnHeaderOptions, TimelineModel } from './model';
|
||||
import { timelineDefaults } from './defaults';
|
||||
import { TimelineById } from './types';
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ import {
|
|||
} from './helpers';
|
||||
|
||||
import { TimelineState, EMPTY_TIMELINE_BY_ID } from './types';
|
||||
import { TimelineType } from '../../../../common/types/timeline';
|
||||
import { TimelineType, TimelineTabs } from '../../../../common/types/timeline';
|
||||
|
||||
export const initialTimelineState: TimelineState = {
|
||||
timelineById: EMPTY_TIMELINE_BY_ID,
|
||||
|
@ -178,16 +178,22 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
|
|||
...state,
|
||||
timelineById: addTimelineNoteToEvent({ id, noteId, eventId, timelineById: state.timelineById }),
|
||||
}))
|
||||
.case(toggleExpandedEvent, (state, { timelineId, event = {} }) => ({
|
||||
...state,
|
||||
timelineById: {
|
||||
...state.timelineById,
|
||||
[timelineId]: {
|
||||
...state.timelineById[timelineId],
|
||||
expandedEvent: event,
|
||||
.case(toggleExpandedEvent, (state, { tabType, timelineId, event = {} }) => {
|
||||
const expandedTabType = tabType ?? TimelineTabs.query;
|
||||
return {
|
||||
...state,
|
||||
timelineById: {
|
||||
...state.timelineById,
|
||||
[timelineId]: {
|
||||
...state.timelineById[timelineId],
|
||||
expandedEvent: {
|
||||
...state.timelineById[timelineId].expandedEvent,
|
||||
[expandedTabType]: event,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
};
|
||||
})
|
||||
.case(addProvider, (state, { id, provider }) => ({
|
||||
...state,
|
||||
timelineById: addTimelineProvider({ id, provider, timelineById: state.timelineById }),
|
||||
|
|
Loading…
Reference in a new issue