mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] Use session view plugin to render session viewer in alerts, events and timeline (#127520)
This commit is contained in:
parent
bde0262191
commit
33b85f8968
52 changed files with 1434 additions and 536 deletions
|
@ -11,6 +11,9 @@ export interface ProcessEcs {
|
|||
Ext?: Ext;
|
||||
command_line?: string[];
|
||||
entity_id?: string[];
|
||||
entry_leader?: ProcessSessionData;
|
||||
session_leader?: ProcessSessionData;
|
||||
group_leader?: ProcessSessionData;
|
||||
exit_code?: number[];
|
||||
hash?: ProcessHashData;
|
||||
parent?: ProcessParentData;
|
||||
|
@ -25,6 +28,12 @@ export interface ProcessEcs {
|
|||
working_directory?: string[];
|
||||
}
|
||||
|
||||
export interface ProcessSessionData {
|
||||
entity_id?: string[];
|
||||
pid?: string[];
|
||||
name?: string[];
|
||||
}
|
||||
|
||||
export interface ProcessHashData {
|
||||
md5?: string[];
|
||||
sha1?: string[];
|
||||
|
|
|
@ -482,6 +482,7 @@ export enum TimelineTabs {
|
|||
notes = 'notes',
|
||||
pinned = 'pinned',
|
||||
eql = 'eql',
|
||||
session = 'session',
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"licensing",
|
||||
"maps",
|
||||
"ruleRegistry",
|
||||
"sessionView",
|
||||
"taskManager",
|
||||
"timelines",
|
||||
"triggersActionsUi",
|
||||
|
|
|
@ -80,7 +80,7 @@ const AlertsTableComponent: React.FC<Props> = ({
|
|||
const dispatch = useDispatch();
|
||||
const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]);
|
||||
const { filterManager } = useKibana().services.data.query;
|
||||
const ACTION_BUTTON_COUNT = 4;
|
||||
const ACTION_BUTTON_COUNT = 5;
|
||||
|
||||
const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ const EventsQueryTabBodyComponent: React.FC<EventsQueryTabBodyComponentProps> =
|
|||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { globalFullScreen } = useGlobalFullScreen();
|
||||
const ACTION_BUTTON_COUNT = 4;
|
||||
const ACTION_BUTTON_COUNT = 5;
|
||||
const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -9,9 +9,8 @@ import React from 'react';
|
|||
import useResizeObserver from 'use-resize-observer/polyfilled';
|
||||
|
||||
import '../../mock/match_media';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { TestProviders } from '../../mock';
|
||||
import { useMountAppended } from '../../utils/use_mount_appended';
|
||||
|
||||
import { mockEventViewerResponse } from './mock';
|
||||
import { StatefulEventsViewer } from '.';
|
||||
|
@ -61,37 +60,27 @@ const testProps = {
|
|||
start: from,
|
||||
};
|
||||
describe('StatefulEventsViewer', () => {
|
||||
const mount = useMountAppended();
|
||||
|
||||
(useTimelineEvents as jest.Mock).mockReturnValue([false, mockEventViewerResponse]);
|
||||
|
||||
test('it renders the events viewer', async () => {
|
||||
const wrapper = mount(
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<StatefulEventsViewer {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.text()).toMatchInlineSnapshot(`"hello grid"`);
|
||||
});
|
||||
expect(wrapper.getByText('hello grid')).toBeTruthy();
|
||||
});
|
||||
|
||||
// InspectButtonContainer controls displaying InspectButton components
|
||||
test('it renders InspectButtonContainer', async () => {
|
||||
const wrapper = mount(
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<StatefulEventsViewer {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find(`InspectButtonContainer`).exists()).toBe(true);
|
||||
});
|
||||
expect(wrapper.getByTestId(`hoverVisibilityContainer`)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it closes field editor when unmounted', async () => {
|
||||
|
@ -101,14 +90,14 @@ describe('StatefulEventsViewer', () => {
|
|||
return {};
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
const { unmount } = render(
|
||||
<TestProviders>
|
||||
<StatefulEventsViewer {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(mockCloseEditor).not.toHaveBeenCalled();
|
||||
|
||||
wrapper.unmount();
|
||||
unmount();
|
||||
expect(mockCloseEditor).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,7 +24,6 @@ import { SourcererScopeName } from '../../store/sourcerer/model';
|
|||
import { useSourcererDataView } from '../../containers/sourcerer';
|
||||
import type { EntityType } from '../../../../../timelines/common';
|
||||
import { TGridCellAction } from '../../../../../timelines/common/types';
|
||||
import { DetailsPanel } from '../../../timelines/components/side_panel';
|
||||
import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
|
||||
import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../lib/cell_actions/constants';
|
||||
import { useGetUserCasesPermissions, useKibana } from '../../lib/kibana';
|
||||
|
@ -33,6 +32,7 @@ import {
|
|||
useFieldBrowserOptions,
|
||||
FieldEditorActions,
|
||||
} from '../../../timelines/components/fields_browser';
|
||||
import { useSessionView } from '../../../timelines/components/timeline/session_tab_content/use_session_view';
|
||||
|
||||
const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = [];
|
||||
|
||||
|
@ -105,6 +105,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
itemsPerPage,
|
||||
itemsPerPageOptions,
|
||||
kqlMode,
|
||||
sessionViewId,
|
||||
showCheckboxes,
|
||||
sort,
|
||||
} = defaultModel,
|
||||
|
@ -155,11 +156,19 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
|
||||
const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]);
|
||||
const trailingControlColumns: ControlColumnProps[] = EMPTY_CONTROL_COLUMNS;
|
||||
const graphOverlay = useMemo(
|
||||
() =>
|
||||
graphEventId != null && graphEventId.length > 0 ? <GraphOverlay timelineId={id} /> : null,
|
||||
[graphEventId, id]
|
||||
);
|
||||
|
||||
const { DetailsPanel, SessionView, Navigation } = useSessionView({
|
||||
entityType,
|
||||
timelineId: id,
|
||||
});
|
||||
|
||||
const graphOverlay = useMemo(() => {
|
||||
const shouldShowOverlay =
|
||||
(graphEventId != null && graphEventId.length > 0) || sessionViewId !== null;
|
||||
return shouldShowOverlay ? (
|
||||
<GraphOverlay timelineId={id} SessionView={SessionView} Navigation={Navigation} />
|
||||
) : null;
|
||||
}, [graphEventId, id, sessionViewId, SessionView, Navigation]);
|
||||
const setQuery = useCallback(
|
||||
(inspect, loading, refetch) => {
|
||||
dispatch(inputsActions.setQuery({ id, inputId: 'global', inspect, loading, refetch }));
|
||||
|
@ -239,14 +248,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
})}
|
||||
</InspectButtonContainer>
|
||||
</FullScreenContainer>
|
||||
<DetailsPanel
|
||||
browserFields={browserFields}
|
||||
entityType={entityType}
|
||||
docValueFields={docValueFields}
|
||||
isFlyoutView
|
||||
runtimeMappings={runtimeMappings}
|
||||
timelineId={id}
|
||||
/>
|
||||
{DetailsPanel}
|
||||
</CasesContext>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -321,6 +321,7 @@ export const mockGlobalState: State = {
|
|||
end: '2020-07-08T08:20:18.966Z',
|
||||
},
|
||||
selectedEventIds: {},
|
||||
sessionViewId: null,
|
||||
show: false,
|
||||
showCheckboxes: false,
|
||||
pinnedEventIds: {},
|
||||
|
|
|
@ -2011,6 +2011,7 @@ export const mockTimelineModel: TimelineModel = {
|
|||
savedObjectId: 'ef579e40-jibber-jabber',
|
||||
selectAll: false,
|
||||
selectedEventIds: {},
|
||||
sessionViewId: null,
|
||||
show: false,
|
||||
showCheckboxes: false,
|
||||
sort: [
|
||||
|
@ -2132,6 +2133,7 @@ export const defaultTimelineProps: CreateTimelineProps = {
|
|||
savedObjectId: null,
|
||||
selectAll: false,
|
||||
selectedEventIds: {},
|
||||
sessionViewId: null,
|
||||
show: false,
|
||||
showCheckboxes: false,
|
||||
sort: [{ columnId: '@timestamp', columnType: 'number', sortDirection: Direction.desc }],
|
||||
|
|
|
@ -312,6 +312,7 @@ describe('alert actions', () => {
|
|||
savedObjectId: null,
|
||||
selectAll: false,
|
||||
selectedEventIds: {},
|
||||
sessionViewId: null,
|
||||
show: true,
|
||||
showCheckboxes: false,
|
||||
sort: [
|
||||
|
|
|
@ -176,4 +176,5 @@ export const requiredFieldsForActions = [
|
|||
'file.hash.sha256',
|
||||
'host.os.family',
|
||||
'event.code',
|
||||
'process.entry_leader.entity_id',
|
||||
];
|
||||
|
|
|
@ -104,7 +104,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
const kibana = useKibana();
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
const { addWarning } = useAppToasts();
|
||||
const ACTION_BUTTON_COUNT = 4;
|
||||
const ACTION_BUTTON_COUNT = 5;
|
||||
|
||||
const getGlobalQuery = useCallback(
|
||||
(customFilters: Filter[]) => {
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { mount } from 'enzyme';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import {
|
||||
useGlobalFullScreen,
|
||||
useTimelineFullScreen,
|
||||
|
@ -37,6 +37,24 @@ jest.mock('../../../resolver/view/use_state_syncing_actions');
|
|||
const useStateSyncingActionsMock = useStateSyncingActions as jest.Mock;
|
||||
|
||||
jest.mock('../../../resolver/view/use_sync_selected_node');
|
||||
jest.mock('../../../common/lib/kibana', () => {
|
||||
const original = jest.requireActual('../../../common/lib/kibana');
|
||||
return {
|
||||
...original,
|
||||
useKibana: () => ({
|
||||
services: {
|
||||
sessionView: {
|
||||
getSessionView: () => <div />,
|
||||
},
|
||||
data: {
|
||||
search: {
|
||||
search: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('GraphOverlay', () => {
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
|
@ -54,20 +72,18 @@ describe('GraphOverlay', () => {
|
|||
});
|
||||
|
||||
describe('when used in an events viewer (i.e. in the Detections view, or the Host > Events view)', () => {
|
||||
test('it has 100% width when NOT in full screen mode', async () => {
|
||||
const wrapper = mount(
|
||||
test('it has 100% width when NOT in full screen mode', () => {
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<GraphOverlay timelineId={TimelineId.test} />
|
||||
<GraphOverlay timelineId={TimelineId.test} SessionView={<div />} Navigation={<div />} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const overlayContainer = wrapper.find('[data-test-subj="overlayContainer"]').first();
|
||||
expect(overlayContainer).toHaveStyleRule('width', '100%');
|
||||
});
|
||||
const overlayContainer = wrapper.getByTestId('overlayContainer');
|
||||
expect(overlayContainer).toHaveStyleRule('width', '100%');
|
||||
});
|
||||
|
||||
test('it has a fixed position when in full screen mode', async () => {
|
||||
test('it has a fixed position when in full screen mode', () => {
|
||||
(useGlobalFullScreen as jest.Mock).mockReturnValue({
|
||||
globalFullScreen: true,
|
||||
setGlobalFullScreen: jest.fn(),
|
||||
|
@ -77,20 +93,18 @@ describe('GraphOverlay', () => {
|
|||
setTimelineFullScreen: jest.fn(),
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<GraphOverlay timelineId={TimelineId.test} />
|
||||
<GraphOverlay timelineId={TimelineId.test} SessionView={<div />} Navigation={<div />} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const overlayContainer = wrapper.find('[data-test-subj="overlayContainer"]').first();
|
||||
expect(overlayContainer).toHaveStyleRule('position', 'fixed');
|
||||
});
|
||||
const overlayContainer = wrapper.getByTestId('overlayContainer');
|
||||
expect(overlayContainer).toHaveStyleRule('position', 'fixed');
|
||||
});
|
||||
|
||||
test('it gets index pattern from default data view', () => {
|
||||
mount(
|
||||
render(
|
||||
<TestProviders
|
||||
store={createStore(
|
||||
{
|
||||
|
@ -110,7 +124,7 @@ describe('GraphOverlay', () => {
|
|||
storage
|
||||
)}
|
||||
>
|
||||
<GraphOverlay timelineId={TimelineId.test} />
|
||||
<GraphOverlay timelineId={TimelineId.test} SessionView={<div />} Navigation={<div />} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -123,20 +137,18 @@ describe('GraphOverlay', () => {
|
|||
describe('when used in the active timeline', () => {
|
||||
const timelineId = TimelineId.active;
|
||||
|
||||
test('it has 100% width when NOT in full screen mode', async () => {
|
||||
const wrapper = mount(
|
||||
test('it has 100% width when NOT in full screen mode', () => {
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<GraphOverlay timelineId={timelineId} />
|
||||
<GraphOverlay timelineId={timelineId} SessionView={<div />} Navigation={<div />} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const overlayContainer = wrapper.find('[data-test-subj="overlayContainer"]').first();
|
||||
expect(overlayContainer).toHaveStyleRule('width', '100%');
|
||||
});
|
||||
const overlayContainer = wrapper.getByTestId('overlayContainer');
|
||||
expect(overlayContainer).toHaveStyleRule('width', '100%');
|
||||
});
|
||||
|
||||
test('it has 100% width when the active timeline is in full screen mode', async () => {
|
||||
test('it has 100% width when the active timeline is in full screen mode', () => {
|
||||
(useGlobalFullScreen as jest.Mock).mockReturnValue({
|
||||
globalFullScreen: false,
|
||||
setGlobalFullScreen: jest.fn(),
|
||||
|
@ -146,20 +158,18 @@ describe('GraphOverlay', () => {
|
|||
setTimelineFullScreen: jest.fn(),
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<GraphOverlay timelineId={timelineId} />
|
||||
<GraphOverlay timelineId={timelineId} SessionView={<div />} Navigation={<div />} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const overlayContainer = wrapper.find('[data-test-subj="overlayContainer"]').first();
|
||||
expect(overlayContainer).toHaveStyleRule('width', '100%');
|
||||
});
|
||||
const overlayContainer = wrapper.getByTestId('overlayContainer');
|
||||
expect(overlayContainer).toHaveStyleRule('width', '100%');
|
||||
});
|
||||
|
||||
test('it gets index pattern from Timeline data view', () => {
|
||||
mount(
|
||||
render(
|
||||
<TestProviders
|
||||
store={createStore(
|
||||
{
|
||||
|
@ -189,11 +199,52 @@ describe('GraphOverlay', () => {
|
|||
storage
|
||||
)}
|
||||
>
|
||||
<GraphOverlay timelineId={timelineId} />
|
||||
<GraphOverlay timelineId={timelineId} SessionView={<div />} Navigation={<div />} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(useStateSyncingActionsMock.mock.calls[0][0].indices).toEqual(mockIndexNames);
|
||||
});
|
||||
|
||||
test('it renders session view controls', () => {
|
||||
(useGlobalFullScreen as jest.Mock).mockReturnValue({
|
||||
globalFullScreen: false,
|
||||
setGlobalFullScreen: jest.fn(),
|
||||
});
|
||||
(useTimelineFullScreen as jest.Mock).mockReturnValue({
|
||||
timelineFullScreen: true,
|
||||
setTimelineFullScreen: jest.fn(),
|
||||
});
|
||||
|
||||
const wrapper = render(
|
||||
<TestProviders
|
||||
store={createStore(
|
||||
{
|
||||
...mockGlobalState,
|
||||
timeline: {
|
||||
...mockGlobalState.timeline,
|
||||
timelineById: {
|
||||
[timelineId]: {
|
||||
...mockGlobalState.timeline.timelineById[timelineId],
|
||||
sessionViewId: 'testId',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
storage
|
||||
)}
|
||||
>
|
||||
<GraphOverlay
|
||||
timelineId={timelineId}
|
||||
SessionView={<div />}
|
||||
Navigation={<div>{'Close Session'}</div>}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.getByText('Close Session')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,25 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiToolTip,
|
||||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useEffect } from 'react';
|
||||
import React, { useMemo, useEffect } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { FULL_SCREEN } from '../timeline/body/column_headers/translations';
|
||||
import { EXIT_FULL_SCREEN } from '../../../common/components/exit_full_screen/translations';
|
||||
import {
|
||||
FULL_SCREEN_TOGGLED_CLASS_NAME,
|
||||
SCROLLING_DISABLED_CLASS_NAME,
|
||||
} from '../../../../common/constants';
|
||||
import {
|
||||
useGlobalFullScreen,
|
||||
useTimelineFullScreen,
|
||||
|
@ -33,7 +18,6 @@ import { TimelineId } from '../../../../common/types/timeline';
|
|||
import { timelineSelectors } from '../../store/timeline';
|
||||
import { timelineDefaults } from '../../store/timeline/defaults';
|
||||
import { isFullScreen } from '../timeline/body/column_headers';
|
||||
import { updateTimelineGraphEventId } from '../../../timelines/store/timeline/actions';
|
||||
import { inputsActions } from '../../../common/store/actions';
|
||||
import { Resolver } from '../../../resolver/view';
|
||||
import {
|
||||
|
@ -41,7 +25,6 @@ import {
|
|||
startSelector,
|
||||
endSelector,
|
||||
} from '../../../common/components/super_date_picker/selectors';
|
||||
import * as i18n from './translations';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||
import { sourcererSelectors } from '../../../common/store';
|
||||
|
@ -66,71 +49,35 @@ const StyledResolver = styled(Resolver)`
|
|||
height: 100%;
|
||||
`;
|
||||
|
||||
const FullScreenButtonIcon = styled(EuiButtonIcon)`
|
||||
margin: 4px 0 4px 0;
|
||||
const ScrollableFlexItem = styled(EuiFlexItem)`
|
||||
${({ theme }) => `margin: 0 ${theme.eui.euiSizeM};`}
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
interface OwnProps {
|
||||
interface GraphOverlayProps {
|
||||
timelineId: TimelineId;
|
||||
SessionView: JSX.Element | null;
|
||||
Navigation: JSX.Element | null;
|
||||
}
|
||||
|
||||
interface NavigationProps {
|
||||
fullScreen: boolean;
|
||||
globalFullScreen: boolean;
|
||||
onCloseOverlay: () => void;
|
||||
timelineId: TimelineId;
|
||||
timelineFullScreen: boolean;
|
||||
toggleFullScreen: () => void;
|
||||
}
|
||||
|
||||
const NavigationComponent: React.FC<NavigationProps> = ({
|
||||
fullScreen,
|
||||
globalFullScreen,
|
||||
onCloseOverlay,
|
||||
const GraphOverlayComponent: React.FC<GraphOverlayProps> = ({
|
||||
timelineId,
|
||||
timelineFullScreen,
|
||||
toggleFullScreen,
|
||||
}) => (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="cross" onClick={onCloseOverlay} size="xs">
|
||||
{i18n.CLOSE_ANALYZER}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
{timelineId !== TimelineId.active && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip content={fullScreen ? EXIT_FULL_SCREEN : FULL_SCREEN}>
|
||||
<FullScreenButtonIcon
|
||||
aria-label={
|
||||
isFullScreen({ globalFullScreen, timelineId, timelineFullScreen })
|
||||
? EXIT_FULL_SCREEN
|
||||
: FULL_SCREEN
|
||||
}
|
||||
className={fullScreen ? FULL_SCREEN_TOGGLED_CLASS_NAME : ''}
|
||||
color={fullScreen ? 'ghost' : 'primary'}
|
||||
data-test-subj="full-screen"
|
||||
iconType="fullScreen"
|
||||
onClick={toggleFullScreen}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
NavigationComponent.displayName = 'NavigationComponent';
|
||||
|
||||
const Navigation = React.memo(NavigationComponent);
|
||||
|
||||
const GraphOverlayComponent: React.FC<OwnProps> = ({ timelineId }) => {
|
||||
SessionView,
|
||||
Navigation,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
|
||||
const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen();
|
||||
const { globalFullScreen } = useGlobalFullScreen();
|
||||
const { timelineFullScreen } = useTimelineFullScreen();
|
||||
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const graphEventId = useDeepEqualSelector(
|
||||
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).graphEventId
|
||||
);
|
||||
const sessionViewId = useDeepEqualSelector(
|
||||
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).sessionViewId
|
||||
);
|
||||
|
||||
const getStartSelector = useMemo(() => startSelector(), []);
|
||||
const getEndSelector = useMemo(() => endSelector(), []);
|
||||
const getIsLoadingSelector = useMemo(() => isLoadingSelector(), []);
|
||||
|
@ -163,24 +110,6 @@ const GraphOverlayComponent: React.FC<OwnProps> = ({ timelineId }) => {
|
|||
);
|
||||
|
||||
const isInTimeline = timelineId === TimelineId.active;
|
||||
const onCloseOverlay = useCallback(() => {
|
||||
const isDataGridFullScreen = document.querySelector('.euiDataGrid--fullScreen') !== null;
|
||||
// Since EUI changes these values directly as a side effect, need to add them back on close.
|
||||
if (isDataGridFullScreen) {
|
||||
if (timelineId === TimelineId.active) {
|
||||
document.body.classList.add('euiDataGrid__restrictBody');
|
||||
} else {
|
||||
document.body.classList.add(SCROLLING_DISABLED_CLASS_NAME, 'euiDataGrid__restrictBody');
|
||||
}
|
||||
} else {
|
||||
if (timelineId === TimelineId.active) {
|
||||
setTimelineFullScreen(false);
|
||||
} else {
|
||||
setGlobalFullScreen(false);
|
||||
}
|
||||
}
|
||||
dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' }));
|
||||
}, [dispatch, timelineId, setTimelineFullScreen, setGlobalFullScreen]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
@ -192,20 +121,6 @@ const GraphOverlayComponent: React.FC<OwnProps> = ({ timelineId }) => {
|
|||
};
|
||||
}, [dispatch, timelineId]);
|
||||
|
||||
const toggleFullScreen = useCallback(() => {
|
||||
if (timelineId === TimelineId.active) {
|
||||
setTimelineFullScreen(!timelineFullScreen);
|
||||
} else {
|
||||
setGlobalFullScreen(!globalFullScreen);
|
||||
}
|
||||
}, [
|
||||
timelineId,
|
||||
setTimelineFullScreen,
|
||||
timelineFullScreen,
|
||||
setGlobalFullScreen,
|
||||
globalFullScreen,
|
||||
]);
|
||||
|
||||
const getDefaultDataViewSelector = useMemo(
|
||||
() => sourcererSelectors.defaultDataViewSelector(),
|
||||
[]
|
||||
|
@ -219,21 +134,32 @@ const GraphOverlayComponent: React.FC<OwnProps> = ({ timelineId }) => {
|
|||
[defaultDataView.patternList, isInTimeline, timelinePatterns]
|
||||
);
|
||||
|
||||
if (fullScreen && !isInTimeline) {
|
||||
if (!isInTimeline && sessionViewId !== null) {
|
||||
if (fullScreen) {
|
||||
return (
|
||||
<FullScreenOverlayContainer data-test-subj="overlayContainer">
|
||||
<EuiFlexGroup alignItems="flexStart" gutterSize="none" direction="column">
|
||||
<EuiFlexItem grow={false}>{Navigation}</EuiFlexItem>
|
||||
<ScrollableFlexItem grow={2}>{SessionView}</ScrollableFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</FullScreenOverlayContainer>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<OverlayContainer data-test-subj="overlayContainer">
|
||||
<EuiFlexGroup alignItems="flexStart" gutterSize="none" direction="column">
|
||||
<EuiFlexItem grow={false}>{Navigation}</EuiFlexItem>
|
||||
<ScrollableFlexItem grow={2}>{SessionView}</ScrollableFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</OverlayContainer>
|
||||
);
|
||||
}
|
||||
} else if (fullScreen && !isInTimeline) {
|
||||
return (
|
||||
<FullScreenOverlayContainer data-test-subj="overlayContainer">
|
||||
<EuiHorizontalRule margin="none" />
|
||||
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<Navigation
|
||||
fullScreen={fullScreen}
|
||||
globalFullScreen={globalFullScreen}
|
||||
onCloseOverlay={onCloseOverlay}
|
||||
timelineId={timelineId}
|
||||
timelineFullScreen={timelineFullScreen}
|
||||
toggleFullScreen={toggleFullScreen}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{Navigation}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
{graphEventId !== undefined ? (
|
||||
|
@ -256,16 +182,7 @@ const GraphOverlayComponent: React.FC<OwnProps> = ({ timelineId }) => {
|
|||
<OverlayContainer data-test-subj="overlayContainer">
|
||||
<EuiHorizontalRule margin="none" />
|
||||
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<Navigation
|
||||
fullScreen={fullScreen}
|
||||
globalFullScreen={globalFullScreen}
|
||||
onCloseOverlay={onCloseOverlay}
|
||||
timelineId={timelineId}
|
||||
timelineFullScreen={timelineFullScreen}
|
||||
toggleFullScreen={toggleFullScreen}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{Navigation}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
{graphEventId !== undefined ? (
|
||||
|
|
|
@ -380,6 +380,313 @@ Array [
|
|||
runtimeMappings={Object {}}
|
||||
tabType="query"
|
||||
timelineId="test"
|
||||
>
|
||||
<CasesContextMock
|
||||
owner={
|
||||
Array [
|
||||
"securitySolution",
|
||||
]
|
||||
}
|
||||
userCanCrud={false}
|
||||
>
|
||||
<EuiFlyoutHeader
|
||||
hasBorder={false}
|
||||
>
|
||||
<div
|
||||
className="euiFlyoutHeader"
|
||||
>
|
||||
<ExpandableEventTitle
|
||||
isAlert={false}
|
||||
loading={true}
|
||||
ruleName=""
|
||||
timestamp=""
|
||||
>
|
||||
<Styled(EuiFlexGroup)
|
||||
gutterSize="none"
|
||||
justifyContent="spaceBetween"
|
||||
wrap={true}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
className="c0"
|
||||
gutterSize="none"
|
||||
justifyContent="spaceBetween"
|
||||
wrap={true}
|
||||
>
|
||||
<div
|
||||
className="euiFlexGroup euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive euiFlexGroup--wrap c0"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</div>
|
||||
</EuiFlexGroup>
|
||||
</Styled(EuiFlexGroup)>
|
||||
</ExpandableEventTitle>
|
||||
</div>
|
||||
</EuiFlyoutHeader>
|
||||
<Styled(EuiFlyoutBody)>
|
||||
<EuiFlyoutBody
|
||||
className="c1"
|
||||
>
|
||||
<div
|
||||
className="euiFlyoutBody c1"
|
||||
>
|
||||
<div
|
||||
className="euiFlyoutBody__overflow"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="euiFlyoutBody__overflowContent"
|
||||
>
|
||||
<ExpandableEvent
|
||||
browserFields={Object {}}
|
||||
detailsData={null}
|
||||
event={
|
||||
Object {
|
||||
"eventId": "my-id",
|
||||
"indexName": "my-index",
|
||||
}
|
||||
}
|
||||
handleOnEventClosed={[Function]}
|
||||
hostRisk={null}
|
||||
isAlert={false}
|
||||
isDraggable={false}
|
||||
loading={true}
|
||||
timelineId="test"
|
||||
timelineTabType="flyout"
|
||||
>
|
||||
<EuiLoadingContent
|
||||
lines={10}
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="0"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="1"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="2"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="3"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="4"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="5"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="6"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="7"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="8"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="9"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</EuiLoadingContent>
|
||||
</ExpandableEvent>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EuiFlyoutBody>
|
||||
</Styled(EuiFlyoutBody)>
|
||||
<Connect(Component)
|
||||
detailsData={null}
|
||||
detailsEcsData={null}
|
||||
expandedEvent={
|
||||
Object {
|
||||
"eventId": "my-id",
|
||||
"indexName": "my-index",
|
||||
}
|
||||
}
|
||||
handleOnEventClosed={[Function]}
|
||||
isHostIsolationPanelOpen={false}
|
||||
loadingEventDetails={true}
|
||||
onAddIsolationStatusClick={[Function]}
|
||||
refetchFlyoutData={[Function]}
|
||||
timelineId="test"
|
||||
>
|
||||
<Memo()
|
||||
detailsData={null}
|
||||
detailsEcsData={null}
|
||||
dispatch={[Function]}
|
||||
expandedEvent={
|
||||
Object {
|
||||
"eventId": "my-id",
|
||||
"indexName": "my-index",
|
||||
}
|
||||
}
|
||||
globalQuery={Array []}
|
||||
handleOnEventClosed={[Function]}
|
||||
isHostIsolationPanelOpen={false}
|
||||
loadingEventDetails={true}
|
||||
onAddIsolationStatusClick={[Function]}
|
||||
refetchFlyoutData={[Function]}
|
||||
timelineId="test"
|
||||
timelineQuery={
|
||||
Object {
|
||||
"id": "",
|
||||
"inspect": null,
|
||||
"isInspected": false,
|
||||
"loading": false,
|
||||
"refetch": null,
|
||||
"selectedInspectIndex": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<EuiFlyoutFooter
|
||||
data-test-subj="side-panel-flyout-footer"
|
||||
>
|
||||
<div
|
||||
className="euiFlyoutFooter"
|
||||
data-test-subj="side-panel-flyout-footer"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
justifyContent="flexEnd"
|
||||
>
|
||||
<div
|
||||
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</div>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</EuiFlyoutFooter>
|
||||
</Memo()>
|
||||
</Connect(Component)>
|
||||
</CasesContextMock>
|
||||
</EventDetailsPanelComponent>
|
||||
</div>
|
||||
</EuiFlyout>,
|
||||
.c0 {
|
||||
-webkit-flex: 0 1 auto;
|
||||
-ms-flex: 0 1 auto;
|
||||
flex: 0 1 auto;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.c1 .euiFlyoutBody__overflow {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.c1 .euiFlyoutBody__overflow .euiFlyoutBody__overflowContent {
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
<div
|
||||
data-eui="EuiFlyout"
|
||||
data-test-subj="timeline:details-panel:flyout"
|
||||
role="dialog"
|
||||
>
|
||||
<button
|
||||
aria-label="Close this dialog"
|
||||
data-test-subj="euiFlyoutCloseButton"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
/>
|
||||
<EventDetailsPanelComponent
|
||||
browserFields={Object {}}
|
||||
docValueFields={Array []}
|
||||
expandedEvent={
|
||||
Object {
|
||||
"eventId": "my-id",
|
||||
"indexName": "my-index",
|
||||
}
|
||||
}
|
||||
handleOnEventClosed={[Function]}
|
||||
isDraggable={false}
|
||||
isFlyoutView={true}
|
||||
runtimeMappings={Object {}}
|
||||
tabType="query"
|
||||
timelineId="test"
|
||||
>
|
||||
<CasesContextMock
|
||||
owner={
|
||||
Array [
|
||||
"securitySolution",
|
||||
]
|
||||
}
|
||||
userCanCrud={false}
|
||||
>
|
||||
<EuiFlyoutHeader
|
||||
hasBorder={false}
|
||||
|
@ -615,296 +922,7 @@ Array [
|
|||
</EuiFlyoutFooter>
|
||||
</Memo()>
|
||||
</Connect(Component)>
|
||||
</EventDetailsPanelComponent>
|
||||
</div>
|
||||
</EuiFlyout>,
|
||||
.c0 {
|
||||
-webkit-flex: 0 1 auto;
|
||||
-ms-flex: 0 1 auto;
|
||||
flex: 0 1 auto;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.c1 .euiFlyoutBody__overflow {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.c1 .euiFlyoutBody__overflow .euiFlyoutBody__overflowContent {
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
<div
|
||||
data-eui="EuiFlyout"
|
||||
data-test-subj="timeline:details-panel:flyout"
|
||||
role="dialog"
|
||||
>
|
||||
<button
|
||||
aria-label="Close this dialog"
|
||||
data-test-subj="euiFlyoutCloseButton"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
/>
|
||||
<EventDetailsPanelComponent
|
||||
browserFields={Object {}}
|
||||
docValueFields={Array []}
|
||||
expandedEvent={
|
||||
Object {
|
||||
"eventId": "my-id",
|
||||
"indexName": "my-index",
|
||||
}
|
||||
}
|
||||
handleOnEventClosed={[Function]}
|
||||
isDraggable={false}
|
||||
isFlyoutView={true}
|
||||
runtimeMappings={Object {}}
|
||||
tabType="query"
|
||||
timelineId="test"
|
||||
>
|
||||
<EuiFlyoutHeader
|
||||
hasBorder={false}
|
||||
>
|
||||
<div
|
||||
className="euiFlyoutHeader"
|
||||
>
|
||||
<ExpandableEventTitle
|
||||
isAlert={false}
|
||||
loading={true}
|
||||
ruleName=""
|
||||
timestamp=""
|
||||
>
|
||||
<Styled(EuiFlexGroup)
|
||||
gutterSize="none"
|
||||
justifyContent="spaceBetween"
|
||||
wrap={true}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
className="c0"
|
||||
gutterSize="none"
|
||||
justifyContent="spaceBetween"
|
||||
wrap={true}
|
||||
>
|
||||
<div
|
||||
className="euiFlexGroup euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive euiFlexGroup--wrap c0"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</div>
|
||||
</EuiFlexGroup>
|
||||
</Styled(EuiFlexGroup)>
|
||||
</ExpandableEventTitle>
|
||||
</div>
|
||||
</EuiFlyoutHeader>
|
||||
<Styled(EuiFlyoutBody)>
|
||||
<EuiFlyoutBody
|
||||
className="c1"
|
||||
>
|
||||
<div
|
||||
className="euiFlyoutBody c1"
|
||||
>
|
||||
<div
|
||||
className="euiFlyoutBody__overflow"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="euiFlyoutBody__overflowContent"
|
||||
>
|
||||
<ExpandableEvent
|
||||
browserFields={Object {}}
|
||||
detailsData={null}
|
||||
event={
|
||||
Object {
|
||||
"eventId": "my-id",
|
||||
"indexName": "my-index",
|
||||
}
|
||||
}
|
||||
handleOnEventClosed={[Function]}
|
||||
hostRisk={null}
|
||||
isAlert={false}
|
||||
isDraggable={false}
|
||||
loading={true}
|
||||
timelineId="test"
|
||||
timelineTabType="flyout"
|
||||
>
|
||||
<EuiLoadingContent
|
||||
lines={10}
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="0"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="1"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="2"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="3"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="4"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="5"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="6"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="7"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="8"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="euiLoadingContent__singleLine"
|
||||
key="9"
|
||||
>
|
||||
<span
|
||||
className="euiLoadingContent__singleLineBackground"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</EuiLoadingContent>
|
||||
</ExpandableEvent>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EuiFlyoutBody>
|
||||
</Styled(EuiFlyoutBody)>
|
||||
<Connect(Component)
|
||||
detailsData={null}
|
||||
detailsEcsData={null}
|
||||
expandedEvent={
|
||||
Object {
|
||||
"eventId": "my-id",
|
||||
"indexName": "my-index",
|
||||
}
|
||||
}
|
||||
handleOnEventClosed={[Function]}
|
||||
isHostIsolationPanelOpen={false}
|
||||
loadingEventDetails={true}
|
||||
onAddIsolationStatusClick={[Function]}
|
||||
refetchFlyoutData={[Function]}
|
||||
timelineId="test"
|
||||
>
|
||||
<Memo()
|
||||
detailsData={null}
|
||||
detailsEcsData={null}
|
||||
dispatch={[Function]}
|
||||
expandedEvent={
|
||||
Object {
|
||||
"eventId": "my-id",
|
||||
"indexName": "my-index",
|
||||
}
|
||||
}
|
||||
globalQuery={Array []}
|
||||
handleOnEventClosed={[Function]}
|
||||
isHostIsolationPanelOpen={false}
|
||||
loadingEventDetails={true}
|
||||
onAddIsolationStatusClick={[Function]}
|
||||
refetchFlyoutData={[Function]}
|
||||
timelineId="test"
|
||||
timelineQuery={
|
||||
Object {
|
||||
"id": "",
|
||||
"inspect": null,
|
||||
"isInspected": false,
|
||||
"loading": false,
|
||||
"refetch": null,
|
||||
"selectedInspectIndex": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<EuiFlyoutFooter
|
||||
data-test-subj="side-panel-flyout-footer"
|
||||
>
|
||||
<div
|
||||
className="euiFlyoutFooter"
|
||||
data-test-subj="side-panel-flyout-footer"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
justifyContent="flexEnd"
|
||||
>
|
||||
<div
|
||||
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</div>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</EuiFlyoutFooter>
|
||||
</Memo()>
|
||||
</Connect(Component)>
|
||||
</CasesContextMock>
|
||||
</EventDetailsPanelComponent>
|
||||
</div>,
|
||||
]
|
||||
|
|
|
@ -193,7 +193,7 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({
|
|||
}
|
||||
|
||||
return isFlyoutView ? (
|
||||
<>
|
||||
<CasesContext owner={[APP_ID]} userCanCrud={casesPermissions?.crud ?? false}>
|
||||
<EuiFlyoutHeader hasBorder={isHostIsolationPanelOpen}>
|
||||
{isHostIsolationPanelOpen ? (
|
||||
backToAlertDetailsLink
|
||||
|
@ -249,7 +249,7 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({
|
|||
onAddIsolationStatusClick={showHostIsolationPanel}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</>
|
||||
</CasesContext>
|
||||
) : (
|
||||
<CasesContext owner={[APP_ID]} userCanCrud={casesPermissions?.crud ?? false}>
|
||||
<ExpandableEventTitle
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useDetailPanel, UseDetailPanelConfig } from './use_detail_panel';
|
||||
import { timelineActions } from '../../../store/timeline';
|
||||
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
|
||||
import { TimelineId, TimelineTabs } from '../../../../../common/types';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../../../common/hooks/use_selector');
|
||||
jest.mock('../../../store/timeline');
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
return {
|
||||
...original,
|
||||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
jest.mock('../../../../common/containers/sourcerer', () => {
|
||||
const mockSourcererReturn = {
|
||||
browserFields: {},
|
||||
docValueFields: [],
|
||||
loading: true,
|
||||
indexPattern: {},
|
||||
selectedPatterns: [],
|
||||
missingPatterns: [],
|
||||
};
|
||||
return {
|
||||
useSourcererDataView: jest.fn().mockReturnValue(mockSourcererReturn),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useDetailPanel', () => {
|
||||
const defaultProps: UseDetailPanelConfig = {
|
||||
sourcererScope: SourcererScopeName.detections,
|
||||
timelineId: TimelineId.test,
|
||||
};
|
||||
const mockGetExpandedDetail = jest.fn().mockImplementation(() => ({}));
|
||||
beforeEach(() => {
|
||||
(useDeepEqualSelector as jest.Mock).mockImplementation((cb) => {
|
||||
return mockGetExpandedDetail();
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
(useDeepEqualSelector as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
test('should return openDetailsPanel fn, handleOnDetailsPanelClosed fn, shouldShowDetailsPanel, and the DetailsPanel component', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => {
|
||||
return useDetailPanel(defaultProps);
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.openDetailsPanel).toBeDefined();
|
||||
expect(result.current.handleOnDetailsPanelClosed).toBeDefined();
|
||||
expect(result.current.shouldShowDetailsPanel).toBe(false);
|
||||
expect(result.current.DetailsPanel).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test('should fire redux action to open details panel', async () => {
|
||||
const testEventId = '123';
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => {
|
||||
return useDetailPanel(defaultProps);
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current?.openDetailsPanel(testEventId);
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalled();
|
||||
expect(timelineActions.toggleDetailPanel).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test('should call provided onClose callback provided to openDetailsPanel fn', async () => {
|
||||
const testEventId = '123';
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => {
|
||||
return useDetailPanel(defaultProps);
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
const mockOnClose = jest.fn();
|
||||
result.current?.openDetailsPanel(testEventId, mockOnClose);
|
||||
result.current?.handleOnDetailsPanelClosed();
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test('should call the last onClose callback provided to openDetailsPanel fn', async () => {
|
||||
// Test that the onClose ref is properly updated
|
||||
const testEventId = '123';
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => {
|
||||
return useDetailPanel(defaultProps);
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
const mockOnClose = jest.fn();
|
||||
const secondMockOnClose = jest.fn();
|
||||
result.current?.openDetailsPanel(testEventId, mockOnClose);
|
||||
result.current?.handleOnDetailsPanelClosed();
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
|
||||
result.current?.openDetailsPanel(testEventId, secondMockOnClose);
|
||||
result.current?.handleOnDetailsPanelClosed();
|
||||
|
||||
expect(secondMockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test('should show the details panel', async () => {
|
||||
mockGetExpandedDetail.mockImplementation(() => ({
|
||||
[TimelineTabs.session]: {
|
||||
panelView: 'somePanel',
|
||||
},
|
||||
}));
|
||||
const updatedProps = {
|
||||
...defaultProps,
|
||||
tabType: TimelineTabs.session,
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => {
|
||||
return useDetailPanel(updatedProps);
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.DetailsPanel).toMatchInlineSnapshot(`
|
||||
<Memo(DetailsPanel)
|
||||
browserFields={Object {}}
|
||||
docValueFields={Array []}
|
||||
handleOnPanelClosed={[Function]}
|
||||
tabType="session"
|
||||
timelineId="test"
|
||||
/>
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback, useRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { timelineActions, timelineSelectors } from '../../../store/timeline';
|
||||
import type { EntityType } from '../../../../../../timelines/common';
|
||||
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
|
||||
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
|
||||
import { activeTimeline } from '../../../containers/active_timeline_context';
|
||||
import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
|
||||
import { timelineDefaults } from '../../../store/timeline/defaults';
|
||||
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { DetailsPanel as DetailsPanelComponent } from '..';
|
||||
|
||||
export interface UseDetailPanelConfig {
|
||||
entityType?: EntityType;
|
||||
isFlyoutView?: boolean;
|
||||
sourcererScope: SourcererScopeName;
|
||||
timelineId: TimelineId;
|
||||
tabType?: TimelineTabs;
|
||||
}
|
||||
|
||||
export interface UseDetailPanelReturn {
|
||||
openDetailsPanel: (eventId?: string, onClose?: () => void) => void;
|
||||
handleOnDetailsPanelClosed: () => void;
|
||||
DetailsPanel: JSX.Element | null;
|
||||
shouldShowDetailsPanel: boolean;
|
||||
}
|
||||
|
||||
export const useDetailPanel = ({
|
||||
entityType,
|
||||
isFlyoutView,
|
||||
sourcererScope,
|
||||
timelineId,
|
||||
tabType,
|
||||
}: UseDetailPanelConfig): UseDetailPanelReturn => {
|
||||
const { browserFields, docValueFields, selectedPatterns, runtimeMappings } =
|
||||
useSourcererDataView(sourcererScope);
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const expandedDetail = useDeepEqualSelector(
|
||||
(state) => (getTimeline(state, timelineId) ?? timelineDefaults)?.expandedDetail
|
||||
);
|
||||
const onPanelClose = useRef(() => {});
|
||||
|
||||
const shouldShowDetailsPanel = useMemo(() => {
|
||||
if (
|
||||
tabType &&
|
||||
expandedDetail &&
|
||||
expandedDetail[tabType] &&
|
||||
!!expandedDetail[tabType]?.panelView
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [expandedDetail, tabType]);
|
||||
|
||||
const loadDetailsPanel = useCallback(
|
||||
(eventId?: string) => {
|
||||
if (eventId) {
|
||||
dispatch(
|
||||
timelineActions.toggleDetailPanel({
|
||||
panelView: 'eventDetail',
|
||||
tabType,
|
||||
timelineId,
|
||||
params: {
|
||||
eventId,
|
||||
indexName: selectedPatterns.join(','),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[dispatch, selectedPatterns, tabType, timelineId]
|
||||
);
|
||||
|
||||
const openDetailsPanel = useCallback(
|
||||
(eventId?: string, onClose?: () => void) => {
|
||||
loadDetailsPanel(eventId);
|
||||
onPanelClose.current = onClose ?? (() => {});
|
||||
},
|
||||
[loadDetailsPanel]
|
||||
);
|
||||
|
||||
const handleOnDetailsPanelClosed = useCallback(() => {
|
||||
if (onPanelClose.current) onPanelClose.current();
|
||||
dispatch(timelineActions.toggleDetailPanel({ tabType, timelineId }));
|
||||
|
||||
if (
|
||||
tabType &&
|
||||
expandedDetail[tabType]?.panelView &&
|
||||
timelineId === TimelineId.active &&
|
||||
shouldShowDetailsPanel
|
||||
) {
|
||||
activeTimeline.toggleExpandedDetail({});
|
||||
}
|
||||
}, [dispatch, timelineId, expandedDetail, tabType, shouldShowDetailsPanel]);
|
||||
|
||||
const DetailsPanel = useMemo(
|
||||
() =>
|
||||
shouldShowDetailsPanel ? (
|
||||
<DetailsPanelComponent
|
||||
browserFields={browserFields}
|
||||
docValueFields={docValueFields}
|
||||
entityType={entityType}
|
||||
handleOnPanelClosed={handleOnDetailsPanelClosed}
|
||||
isFlyoutView={isFlyoutView}
|
||||
runtimeMappings={runtimeMappings}
|
||||
tabType={tabType}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
) : null,
|
||||
[
|
||||
browserFields,
|
||||
docValueFields,
|
||||
entityType,
|
||||
handleOnDetailsPanelClosed,
|
||||
isFlyoutView,
|
||||
runtimeMappings,
|
||||
shouldShowDetailsPanel,
|
||||
tabType,
|
||||
timelineId,
|
||||
]
|
||||
);
|
||||
|
||||
return {
|
||||
openDetailsPanel,
|
||||
handleOnDetailsPanelClosed,
|
||||
shouldShowDetailsPanel,
|
||||
DetailsPanel,
|
||||
};
|
||||
};
|
|
@ -24,6 +24,7 @@ import { useShallowEqualSelector } from '../../../../../common/hooks/use_selecto
|
|||
import {
|
||||
setActiveTabTimeline,
|
||||
updateTimelineGraphEventId,
|
||||
updateTimelineSessionViewSessionId,
|
||||
} from '../../../../store/timeline/actions';
|
||||
import {
|
||||
useGlobalFullScreen,
|
||||
|
@ -128,6 +129,35 @@ const ActionsComponent: React.FC<ActionProps> = ({
|
|||
}
|
||||
}, [dispatch, ecsData._id, timelineId, setGlobalFullScreen, setTimelineFullScreen]);
|
||||
|
||||
const entryLeader = useMemo(() => {
|
||||
const { process } = ecsData;
|
||||
const entryLeaderIds = process?.entry_leader?.entity_id;
|
||||
if (entryLeaderIds !== undefined && entryLeaderIds.length > 0) {
|
||||
return entryLeaderIds[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [ecsData]);
|
||||
|
||||
const openSessionView = useCallback(() => {
|
||||
const dataGridIsFullScreen = document.querySelector('.euiDataGrid--fullScreen');
|
||||
if (timelineId === TimelineId.active) {
|
||||
if (dataGridIsFullScreen) {
|
||||
setTimelineFullScreen(true);
|
||||
}
|
||||
if (entryLeader !== null) {
|
||||
dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.session }));
|
||||
}
|
||||
} else {
|
||||
if (dataGridIsFullScreen) {
|
||||
setGlobalFullScreen(true);
|
||||
}
|
||||
}
|
||||
if (entryLeader !== null) {
|
||||
dispatch(updateTimelineSessionViewSessionId({ id: timelineId, eventId: entryLeader }));
|
||||
}
|
||||
}, [dispatch, timelineId, entryLeader, setGlobalFullScreen, setTimelineFullScreen]);
|
||||
|
||||
return (
|
||||
<ActionsContainer>
|
||||
{showCheckboxes && !tGridEnabled && (
|
||||
|
@ -220,6 +250,21 @@ const ActionsComponent: React.FC<ActionProps> = ({
|
|||
</EventsTdContent>
|
||||
</div>
|
||||
) : null}
|
||||
{entryLeader !== null ? (
|
||||
<div>
|
||||
<EventsTdContent textAlign="center" width={DEFAULT_ACTION_BUTTON_WIDTH}>
|
||||
<EuiToolTip data-test-subj="expand-event-tool-tip" content={i18n.OPEN_SESSION_VIEW}>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.VIEW_DETAILS_FOR_ROW({ ariaRowindex, columnValues })}
|
||||
data-test-subj="session-view-button"
|
||||
iconType="console"
|
||||
onClick={openSessionView}
|
||||
size="s"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EventsTdContent>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
</ActionsContainer>
|
||||
);
|
||||
|
|
|
@ -108,7 +108,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
const { queryFields, selectAll } = useDeepEqualSelector((state) =>
|
||||
getManageTimeline(state, id)
|
||||
);
|
||||
const ACTION_BUTTON_COUNT = 5;
|
||||
const ACTION_BUTTON_COUNT = 6;
|
||||
|
||||
const onRowSelected: OnRowSelected = useCallback(
|
||||
({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => {
|
||||
|
|
|
@ -28,6 +28,13 @@ export const COPY_TO_CLIPBOARD = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const OPEN_SESSION_VIEW = i18n.translate(
|
||||
'xpack.securitySolution.timeline.body.openSessionViewLabel',
|
||||
{
|
||||
defaultMessage: 'Open Session View',
|
||||
}
|
||||
);
|
||||
|
||||
export const INVESTIGATE = i18n.translate(
|
||||
'xpack.securitySolution.timeline.body.actions.investigateLabel',
|
||||
{
|
||||
|
|
|
@ -181,7 +181,7 @@ export const EqlTabContentComponent: React.FC<Props> = ({
|
|||
runtimeMappings,
|
||||
selectedPatterns,
|
||||
} = useSourcererDataView(SourcererScopeName.timeline);
|
||||
const ACTION_BUTTON_COUNT = 5;
|
||||
const ACTION_BUTTON_COUNT = 6;
|
||||
|
||||
const isBlankTimeline: boolean = isEmpty(eqlQuery);
|
||||
|
||||
|
|
|
@ -6,27 +6,54 @@
|
|||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { timelineSelectors } from '../../../store/timeline';
|
||||
import { useShallowEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { TimelineId } from '../../../../../common/types/timeline';
|
||||
import { GraphOverlay } from '../../graph_overlay';
|
||||
import { useSessionView } from '../session_tab_content/use_session_view';
|
||||
|
||||
interface GraphTabContentProps {
|
||||
timelineId: TimelineId;
|
||||
}
|
||||
|
||||
const ScrollableFlexItem = styled(EuiFlexItem)`
|
||||
${({ theme }) => `margin: 0 ${theme.eui.euiSizeM};`}
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const VerticalRule = styled.div`
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: ${({ theme }) => theme.eui.euiColorLightShade};
|
||||
`;
|
||||
|
||||
const GraphTabContentComponent: React.FC<GraphTabContentProps> = ({ timelineId }) => {
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const graphEventId = useShallowEqualSelector(
|
||||
(state) => getTimeline(state, timelineId)?.graphEventId
|
||||
);
|
||||
|
||||
const { shouldShowDetailsPanel, DetailsPanel, Navigation, SessionView } = useSessionView({
|
||||
timelineId,
|
||||
});
|
||||
|
||||
if (!graphEventId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <GraphOverlay timelineId={timelineId} />;
|
||||
return (
|
||||
<>
|
||||
<GraphOverlay timelineId={timelineId} Navigation={Navigation} SessionView={SessionView} />
|
||||
{shouldShowDetailsPanel && (
|
||||
<>
|
||||
<VerticalRule />
|
||||
<ScrollableFlexItem grow={1}>{DetailsPanel}</ScrollableFlexItem>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
GraphTabContentComponent.displayName = 'GraphTabContentComponent';
|
||||
|
|
|
@ -74,9 +74,18 @@ const StatefulTimelineComponent: React.FC<Props> = ({
|
|||
savedObjectId,
|
||||
timelineType,
|
||||
description,
|
||||
sessionViewId,
|
||||
} = useDeepEqualSelector((state) =>
|
||||
pick(
|
||||
['indexNames', 'dataViewId', 'graphEventId', 'savedObjectId', 'timelineType', 'description'],
|
||||
[
|
||||
'indexNames',
|
||||
'dataViewId',
|
||||
'graphEventId',
|
||||
'savedObjectId',
|
||||
'timelineType',
|
||||
'description',
|
||||
'sessionViewId',
|
||||
],
|
||||
getTimeline(state, timelineId) ?? timelineDefaults
|
||||
)
|
||||
);
|
||||
|
@ -193,6 +202,7 @@ const StatefulTimelineComponent: React.FC<Props> = ({
|
|||
|
||||
<TabsContent
|
||||
graphEventId={graphEventId}
|
||||
sessionViewId={sessionViewId}
|
||||
renderCellValue={renderCellValue}
|
||||
rowRenderers={rowRenderers}
|
||||
timelineId={timelineId}
|
||||
|
|
|
@ -125,7 +125,7 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
|
|||
selectedPatterns,
|
||||
} = useSourcererDataView(SourcererScopeName.timeline);
|
||||
const { setTimelineFullScreen, timelineFullScreen } = useTimelineFullScreen();
|
||||
const ACTION_BUTTON_COUNT = 5;
|
||||
const ACTION_BUTTON_COUNT = 6;
|
||||
|
||||
const filterQuery = useMemo(() => {
|
||||
if (isEmpty(pinnedEventIds)) {
|
||||
|
|
|
@ -202,7 +202,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
} = useSourcererDataView(SourcererScopeName.timeline);
|
||||
|
||||
const { uiSettings } = useKibana().services;
|
||||
const ACTION_BUTTON_COUNT = 5;
|
||||
const ACTION_BUTTON_COUNT = 6;
|
||||
|
||||
const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
|
||||
const { filterManager: activeFilterManager } = useDeepEqualSelector((state) =>
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { TimelineId } from '../../../../../common/types/timeline';
|
||||
import { useSessionView } from './use_session_view';
|
||||
|
||||
const FullWidthFlexGroup = styled(EuiFlexGroup)`
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const ScrollableFlexItem = styled(EuiFlexItem)`
|
||||
${({ theme }) => `margin: 0 ${theme.eui.euiSizeM};`}
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const VerticalRule = styled.div`
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: ${({ theme }) => theme.eui.euiColorLightShade};
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
timelineId: TimelineId;
|
||||
}
|
||||
|
||||
const SessionTabContent: React.FC<Props> = ({ timelineId }) => {
|
||||
const { SessionView, shouldShowDetailsPanel, DetailsPanel, Navigation } = useSessionView({
|
||||
timelineId,
|
||||
});
|
||||
|
||||
return (
|
||||
<FullWidthFlexGroup gutterSize="none">
|
||||
<EuiFlexGroup alignItems="flexStart" gutterSize="none" direction="column">
|
||||
<EuiFlexItem grow={false}>{Navigation}</EuiFlexItem>
|
||||
<ScrollableFlexItem>{SessionView}</ScrollableFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{shouldShowDetailsPanel && (
|
||||
<>
|
||||
<VerticalRule />
|
||||
<ScrollableFlexItem grow={1}>{DetailsPanel}</ScrollableFlexItem>
|
||||
</>
|
||||
)}
|
||||
</FullWidthFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default SessionTabContent;
|
|
@ -13,3 +13,10 @@ export const CLOSE_ANALYZER = i18n.translate(
|
|||
defaultMessage: 'Close analyzer',
|
||||
}
|
||||
);
|
||||
|
||||
export const CLOSE_SESSION = i18n.translate(
|
||||
'xpack.securitySolution.timeline.graphOverlay.closeSessionButton',
|
||||
{
|
||||
defaultMessage: 'Close Session',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { TimelineId } from '../../../../../common/types/timeline';
|
||||
import { mockTimelineModel } from '../../../../common/mock';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import {
|
||||
useTimelineFullScreen,
|
||||
useGlobalFullScreen,
|
||||
} from '../../../../common/containers/use_full_screen';
|
||||
import { useSessionView } from './use_session_view';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('../../../../common/hooks/use_selector', () => ({
|
||||
useDeepEqualSelector: () => {
|
||||
return 'test';
|
||||
},
|
||||
useShallowEqualSelector: () => mockTimelineModel,
|
||||
}));
|
||||
|
||||
jest.mock('../../../../common/containers/use_full_screen');
|
||||
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
jest.mock('../../../../common/lib/kibana', () => {
|
||||
const originalModule = jest.requireActual('../../../../common/lib/kibana');
|
||||
return {
|
||||
...originalModule,
|
||||
useKibana: jest.fn().mockReturnValue({
|
||||
services: {
|
||||
application: {
|
||||
navigateToApp: jest.fn(),
|
||||
getUrlForApp: jest.fn(),
|
||||
capabilities: {
|
||||
siem: { crud_alerts: true, read_alerts: true },
|
||||
},
|
||||
},
|
||||
sessionView: {
|
||||
getSessionView: jest.fn().mockReturnValue(<div />),
|
||||
},
|
||||
data: {
|
||||
search: jest.fn(),
|
||||
query: jest.fn(),
|
||||
},
|
||||
uiSettings: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
savedObjects: {
|
||||
client: {},
|
||||
},
|
||||
timelines: {
|
||||
getLastUpdated: jest.fn(),
|
||||
getLoadingPanel: jest.fn(),
|
||||
getFieldBrowser: jest.fn(),
|
||||
getUseDraggableKeyboardWrapper: () =>
|
||||
jest.fn().mockReturnValue({
|
||||
onBlur: jest.fn(),
|
||||
onKeyDown: jest.fn(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
jest.mock('../../side_panel/hooks/use_detail_panel', () => {
|
||||
return {
|
||||
useDetailPanel: () => ({
|
||||
openDetailsPanel: () => {},
|
||||
handleOnDetailsPanelClosed: () => {},
|
||||
DetailsPanel: () => <div />,
|
||||
shouldShowDetailsPanel: false,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useSessionView', () => {
|
||||
let setTimelineFullScreen: jest.Mock;
|
||||
let setGlobalFullScreen: jest.Mock;
|
||||
let kibana: ReturnType<typeof useKibana>;
|
||||
const Wrapper = memo(({ children }) => {
|
||||
kibana = useKibana();
|
||||
return <>{children}</>;
|
||||
});
|
||||
Wrapper.displayName = 'Wrapper';
|
||||
|
||||
beforeEach(() => {
|
||||
setTimelineFullScreen = jest.fn();
|
||||
setGlobalFullScreen = jest.fn();
|
||||
(useTimelineFullScreen as jest.Mock).mockImplementation(() => ({
|
||||
setTimelineFullScreen,
|
||||
}));
|
||||
(useGlobalFullScreen as jest.Mock).mockImplementation(() => ({
|
||||
setGlobalFullScreen,
|
||||
}));
|
||||
});
|
||||
|
||||
it('removes the full screen class from the overlay', () => {
|
||||
renderHook(
|
||||
() => {
|
||||
const testProps = {
|
||||
timelineId: TimelineId.active,
|
||||
};
|
||||
return useSessionView(testProps);
|
||||
},
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
expect(kibana.services.sessionView.getSessionView).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls setTimelineFullScreen with false when onCloseOverlay is called and the app is not in full screen mode', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const testProps = {
|
||||
timelineId: TimelineId.active,
|
||||
};
|
||||
return useSessionView(testProps);
|
||||
},
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
const navigation = result.current.Navigation;
|
||||
|
||||
expect(navigation).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,230 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { EuiButtonEmpty, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import type { EntityType } from '../../../../../../timelines/common';
|
||||
import { timelineSelectors } from '../../../store/timeline';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
|
||||
import { timelineDefaults } from '../../../../timelines/store/timeline/defaults';
|
||||
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { useDetailPanel } from '../../side_panel/hooks/use_detail_panel';
|
||||
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
|
||||
import { isFullScreen } from '../body/column_headers';
|
||||
import {
|
||||
SCROLLING_DISABLED_CLASS_NAME,
|
||||
FULL_SCREEN_TOGGLED_CLASS_NAME,
|
||||
} from '../../../../../common/constants';
|
||||
import { FULL_SCREEN } from '../../timeline/body/column_headers/translations';
|
||||
import { EXIT_FULL_SCREEN } from '../../../../common/components/exit_full_screen/translations';
|
||||
import {
|
||||
useTimelineFullScreen,
|
||||
useGlobalFullScreen,
|
||||
} from '../../../../common/containers/use_full_screen';
|
||||
import {
|
||||
updateTimelineGraphEventId,
|
||||
updateTimelineSessionViewSessionId,
|
||||
setActiveTabTimeline,
|
||||
} from '../../../../timelines/store/timeline/actions';
|
||||
import { detectionsTimelineIds } from '../../../containers/helpers';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const FullScreenButtonIcon = styled(EuiButtonIcon)`
|
||||
margin: 4px 0 4px 0;
|
||||
`;
|
||||
interface NavigationProps {
|
||||
fullScreen: boolean;
|
||||
globalFullScreen: boolean;
|
||||
onCloseOverlay: () => void;
|
||||
timelineId: TimelineId;
|
||||
timelineFullScreen: boolean;
|
||||
toggleFullScreen: () => void;
|
||||
graphEventId?: string;
|
||||
activeTab: TimelineTabs;
|
||||
}
|
||||
|
||||
const NavigationComponent: React.FC<NavigationProps> = ({
|
||||
fullScreen,
|
||||
globalFullScreen,
|
||||
onCloseOverlay,
|
||||
timelineId,
|
||||
timelineFullScreen,
|
||||
toggleFullScreen,
|
||||
graphEventId,
|
||||
activeTab,
|
||||
}) => {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="cross" onClick={onCloseOverlay} size="xs">
|
||||
{activeTab === TimelineTabs.graph ? i18n.CLOSE_ANALYZER : i18n.CLOSE_SESSION}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
{timelineId !== TimelineId.active && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip content={fullScreen ? EXIT_FULL_SCREEN : FULL_SCREEN}>
|
||||
<FullScreenButtonIcon
|
||||
aria-label={
|
||||
isFullScreen({ globalFullScreen, timelineId, timelineFullScreen })
|
||||
? EXIT_FULL_SCREEN
|
||||
: FULL_SCREEN
|
||||
}
|
||||
className={fullScreen ? FULL_SCREEN_TOGGLED_CLASS_NAME : ''}
|
||||
color={fullScreen ? 'ghost' : 'primary'}
|
||||
data-test-subj="full-screen"
|
||||
iconType="fullScreen"
|
||||
onClick={toggleFullScreen}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
NavigationComponent.displayName = 'NavigationComponent';
|
||||
|
||||
const Navigation = React.memo(NavigationComponent);
|
||||
|
||||
export const useSessionView = ({
|
||||
timelineId,
|
||||
entityType,
|
||||
}: {
|
||||
timelineId: TimelineId;
|
||||
entityType?: EntityType;
|
||||
}) => {
|
||||
const { sessionView } = useKibana().services;
|
||||
const dispatch = useDispatch();
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
|
||||
const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen();
|
||||
|
||||
const { graphEventId, sessionViewId, activeTab, prevActiveTab } = useDeepEqualSelector(
|
||||
(state) => getTimeline(state, timelineId) ?? timelineDefaults
|
||||
);
|
||||
const onCloseOverlay = useCallback(() => {
|
||||
const isDataGridFullScreen = document.querySelector('.euiDataGrid--fullScreen') !== null;
|
||||
// Since EUI changes these values directly as a side effect, need to add them back on close.
|
||||
if (isDataGridFullScreen) {
|
||||
if (timelineId === TimelineId.active) {
|
||||
document.body.classList.add('euiDataGrid__restrictBody');
|
||||
} else {
|
||||
document.body.classList.add(SCROLLING_DISABLED_CLASS_NAME, 'euiDataGrid__restrictBody');
|
||||
}
|
||||
} else {
|
||||
if (timelineId === TimelineId.active) {
|
||||
setTimelineFullScreen(false);
|
||||
} else {
|
||||
setGlobalFullScreen(false);
|
||||
}
|
||||
}
|
||||
if (timelineId !== TimelineId.active) {
|
||||
dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' }));
|
||||
dispatch(updateTimelineSessionViewSessionId({ id: timelineId, eventId: null }));
|
||||
} else {
|
||||
if (activeTab === TimelineTabs.graph) {
|
||||
dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' }));
|
||||
if (prevActiveTab === TimelineTabs.session && !sessionViewId) {
|
||||
dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.query }));
|
||||
}
|
||||
} else if (activeTab === TimelineTabs.session) {
|
||||
dispatch(updateTimelineSessionViewSessionId({ id: timelineId, eventId: null }));
|
||||
if (prevActiveTab === TimelineTabs.graph && !graphEventId) {
|
||||
dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.query }));
|
||||
} else {
|
||||
dispatch(setActiveTabTimeline({ id: timelineId, activeTab: prevActiveTab }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
dispatch,
|
||||
timelineId,
|
||||
setTimelineFullScreen,
|
||||
setGlobalFullScreen,
|
||||
activeTab,
|
||||
prevActiveTab,
|
||||
graphEventId,
|
||||
sessionViewId,
|
||||
]);
|
||||
const fullScreen = useMemo(
|
||||
() => isFullScreen({ globalFullScreen, timelineId, timelineFullScreen }),
|
||||
[globalFullScreen, timelineId, timelineFullScreen]
|
||||
);
|
||||
const toggleFullScreen = useCallback(() => {
|
||||
if (timelineId === TimelineId.active) {
|
||||
setTimelineFullScreen(!timelineFullScreen);
|
||||
} else {
|
||||
setGlobalFullScreen(!globalFullScreen);
|
||||
}
|
||||
}, [
|
||||
timelineId,
|
||||
setTimelineFullScreen,
|
||||
timelineFullScreen,
|
||||
setGlobalFullScreen,
|
||||
globalFullScreen,
|
||||
]);
|
||||
const sourcererScope = useMemo(() => {
|
||||
if (timelineId === TimelineId.active) {
|
||||
return SourcererScopeName.timeline;
|
||||
} else if (detectionsTimelineIds.includes(timelineId)) {
|
||||
return SourcererScopeName.detections;
|
||||
} else {
|
||||
return SourcererScopeName.default;
|
||||
}
|
||||
}, [timelineId]);
|
||||
const { openDetailsPanel, shouldShowDetailsPanel, DetailsPanel } = useDetailPanel({
|
||||
isFlyoutView: timelineId !== TimelineId.active,
|
||||
entityType,
|
||||
sourcererScope,
|
||||
timelineId,
|
||||
tabType: timelineId === TimelineId.active ? TimelineTabs.session : TimelineTabs.query,
|
||||
});
|
||||
|
||||
const sessionViewComponent = useMemo(() => {
|
||||
return sessionViewId !== null
|
||||
? sessionView.getSessionView({
|
||||
sessionEntityId: sessionViewId,
|
||||
loadAlertDetails: openDetailsPanel,
|
||||
})
|
||||
: null;
|
||||
}, [openDetailsPanel, sessionView, sessionViewId]);
|
||||
|
||||
const navigation = useMemo(() => {
|
||||
return (
|
||||
<Navigation
|
||||
fullScreen={fullScreen}
|
||||
globalFullScreen={globalFullScreen}
|
||||
activeTab={activeTab}
|
||||
onCloseOverlay={onCloseOverlay}
|
||||
timelineId={timelineId}
|
||||
timelineFullScreen={timelineFullScreen}
|
||||
toggleFullScreen={toggleFullScreen}
|
||||
graphEventId={graphEventId}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
fullScreen,
|
||||
globalFullScreen,
|
||||
activeTab,
|
||||
graphEventId,
|
||||
onCloseOverlay,
|
||||
timelineFullScreen,
|
||||
timelineId,
|
||||
toggleFullScreen,
|
||||
]);
|
||||
|
||||
return {
|
||||
onCloseOverlay,
|
||||
openDetailsPanel,
|
||||
shouldShowDetailsPanel,
|
||||
SessionView: sessionViewComponent,
|
||||
DetailsPanel,
|
||||
Navigation: navigation,
|
||||
};
|
||||
};
|
|
@ -51,6 +51,7 @@ const EqlTabContent = lazy(() => import('../eql_tab_content'));
|
|||
const GraphTabContent = lazy(() => import('../graph_tab_content'));
|
||||
const NotesTabContent = lazy(() => import('../notes_tab_content'));
|
||||
const PinnedTabContent = lazy(() => import('../pinned_tab_content'));
|
||||
const SessionTabContent = lazy(() => import('../session_tab_content'));
|
||||
|
||||
interface BasicTimelineTab {
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
|
@ -59,6 +60,7 @@ interface BasicTimelineTab {
|
|||
timelineId: TimelineId;
|
||||
timelineType: TimelineType;
|
||||
graphEventId?: string;
|
||||
sessionViewId?: string | null;
|
||||
timelineDescription: string;
|
||||
}
|
||||
|
||||
|
@ -106,6 +108,13 @@ const NotesTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) =>
|
|||
));
|
||||
NotesTab.displayName = 'NotesTab';
|
||||
|
||||
const SessionTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => (
|
||||
<Suspense fallback={<EuiLoadingContent lines={10} />}>
|
||||
<SessionTabContent timelineId={timelineId} />
|
||||
</Suspense>
|
||||
));
|
||||
SessionTab.displayName = 'SessionTab';
|
||||
|
||||
const PinnedTab: React.FC<{
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
rowRenderers: RowRenderer[];
|
||||
|
@ -132,6 +141,8 @@ const ActiveTimelineTab = memo<ActiveTimelineTabProps>(
|
|||
return <GraphTab timelineId={timelineId} />;
|
||||
case TimelineTabs.notes:
|
||||
return <NotesTab timelineId={timelineId} />;
|
||||
case TimelineTabs.session:
|
||||
return <SessionTab timelineId={timelineId} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -140,7 +151,8 @@ const ActiveTimelineTab = memo<ActiveTimelineTabProps>(
|
|||
);
|
||||
|
||||
const isGraphOrNotesTabs = useMemo(
|
||||
() => [TimelineTabs.graph, TimelineTabs.notes].includes(activeTimelineTab),
|
||||
() =>
|
||||
[TimelineTabs.graph, TimelineTabs.notes, TimelineTabs.session].includes(activeTimelineTab),
|
||||
[activeTimelineTab]
|
||||
);
|
||||
|
||||
|
@ -223,6 +235,7 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({
|
|||
timelineFullScreen,
|
||||
timelineType,
|
||||
graphEventId,
|
||||
sessionViewId,
|
||||
timelineDescription,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
@ -262,33 +275,36 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({
|
|||
[appNotes, allTimelineNoteIds, timelineDescription]
|
||||
);
|
||||
|
||||
const setActiveTab = useCallback(
|
||||
(tab: TimelineTabs) => {
|
||||
dispatch(timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: tab }));
|
||||
},
|
||||
[dispatch, timelineId]
|
||||
);
|
||||
|
||||
const setQueryAsActiveTab = useCallback(() => {
|
||||
dispatch(
|
||||
timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.query })
|
||||
);
|
||||
}, [dispatch, timelineId]);
|
||||
setActiveTab(TimelineTabs.query);
|
||||
}, [setActiveTab]);
|
||||
|
||||
const setEqlAsActiveTab = useCallback(() => {
|
||||
dispatch(timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.eql }));
|
||||
}, [dispatch, timelineId]);
|
||||
setActiveTab(TimelineTabs.eql);
|
||||
}, [setActiveTab]);
|
||||
|
||||
const setGraphAsActiveTab = useCallback(() => {
|
||||
dispatch(
|
||||
timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.graph })
|
||||
);
|
||||
}, [dispatch, timelineId]);
|
||||
setActiveTab(TimelineTabs.graph);
|
||||
}, [setActiveTab]);
|
||||
|
||||
const setNotesAsActiveTab = useCallback(() => {
|
||||
dispatch(
|
||||
timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.notes })
|
||||
);
|
||||
}, [dispatch, timelineId]);
|
||||
setActiveTab(TimelineTabs.notes);
|
||||
}, [setActiveTab]);
|
||||
|
||||
const setPinnedAsActiveTab = useCallback(() => {
|
||||
dispatch(
|
||||
timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.pinned })
|
||||
);
|
||||
}, [dispatch, timelineId]);
|
||||
setActiveTab(TimelineTabs.pinned);
|
||||
}, [setActiveTab]);
|
||||
|
||||
const setSessionAsActiveTab = useCallback(() => {
|
||||
setActiveTab(TimelineTabs.session);
|
||||
}, [setActiveTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!graphEventId && activeTab === TimelineTabs.graph) {
|
||||
|
@ -331,6 +347,15 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({
|
|||
>
|
||||
{i18n.ANALYZER_TAB}
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
data-test-subj={`timelineTabs-${TimelineTabs.session}`}
|
||||
onClick={setSessionAsActiveTab}
|
||||
isSelected={activeTab === TimelineTabs.session}
|
||||
disabled={sessionViewId === null}
|
||||
key={TimelineTabs.session}
|
||||
>
|
||||
{i18n.SESSION_TAB}
|
||||
</EuiTab>
|
||||
<StyledEuiTab
|
||||
data-test-subj={`timelineTabs-${TimelineTabs.notes}`}
|
||||
onClick={setNotesAsActiveTab}
|
||||
|
|
|
@ -38,3 +38,10 @@ export const PINNED_TAB = i18n.translate(
|
|||
defaultMessage: 'Pinned',
|
||||
}
|
||||
);
|
||||
|
||||
export const SESSION_TAB = i18n.translate(
|
||||
'xpack.securitySolution.timeline.tabs.sessionTabTimelineTitle',
|
||||
{
|
||||
defaultMessage: 'Session View',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -81,6 +81,11 @@ export const updateTimelineGraphEventId = actionCreator<{ id: string; graphEvent
|
|||
'UPDATE_TIMELINE_GRAPH_EVENT_ID'
|
||||
);
|
||||
|
||||
export const updateTimelineSessionViewSessionId = actionCreator<{
|
||||
id: string;
|
||||
eventId: string | null;
|
||||
}>('UPDATE_TIMELINE_SESSION_VIEW_SESSION_ID');
|
||||
|
||||
export const unPinEvent = actionCreator<{ id: string; eventId: string }>('UN_PIN_EVENT');
|
||||
|
||||
export const updateTimeline = actionCreator<{
|
||||
|
|
|
@ -65,6 +65,7 @@ export const timelineDefaults: SubsetTimelineModel &
|
|||
savedObjectId: null,
|
||||
selectAll: false,
|
||||
selectedEventIds: {},
|
||||
sessionViewId: null,
|
||||
show: false,
|
||||
showCheckboxes: false,
|
||||
sort: [
|
||||
|
|
|
@ -159,6 +159,7 @@ describe('Epic Timeline', () => {
|
|||
dateRange: { start: '2019-10-30T21:06:27.644Z', end: '2019-10-31T21:06:27.644Z' },
|
||||
savedObjectId: '11169110-fc22-11e9-8ca9-072f15ce2685',
|
||||
selectedEventIds: {},
|
||||
sessionViewId: null,
|
||||
show: true,
|
||||
showCheckboxes: false,
|
||||
sort: [{ columnId: '@timestamp', columnType: 'number', sortDirection: Direction.desc }],
|
||||
|
|
|
@ -287,6 +287,26 @@ export const updateGraphEventId = ({
|
|||
};
|
||||
};
|
||||
|
||||
export const updateSessionViewSessionId = ({
|
||||
id,
|
||||
eventId,
|
||||
timelineById,
|
||||
}: {
|
||||
id: string;
|
||||
eventId: string | null;
|
||||
timelineById: TimelineById;
|
||||
}): TimelineById => {
|
||||
const timeline = timelineById[id];
|
||||
|
||||
return {
|
||||
...timelineById,
|
||||
[id]: {
|
||||
...timeline,
|
||||
sessionViewId: eventId,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const queryMatchCustomizer = (dp1: QueryMatch, dp2: QueryMatch) => {
|
||||
if (dp1.field === dp2.field && dp1.value === dp2.value && dp1.operator === dp2.operator) {
|
||||
return true;
|
||||
|
|
|
@ -63,6 +63,7 @@ export type TimelineModel = TGridModelForTimeline & {
|
|||
resolveTimelineConfig?: ResolveTimelineConfig;
|
||||
showSaveModal?: boolean;
|
||||
savedQueryId?: string | null;
|
||||
sessionViewId: string | null;
|
||||
/** When true, show the timeline flyover */
|
||||
show: boolean;
|
||||
/** status: active | draft */
|
||||
|
@ -118,6 +119,7 @@ export type SubsetTimelineModel = Readonly<
|
|||
| 'dateRange'
|
||||
| 'selectAll'
|
||||
| 'selectedEventIds'
|
||||
| 'sessionViewId'
|
||||
| 'show'
|
||||
| 'showCheckboxes'
|
||||
| 'sort'
|
||||
|
|
|
@ -123,6 +123,7 @@ const basicTimeline: TimelineModel = {
|
|||
savedObjectId: null,
|
||||
selectAll: false,
|
||||
selectedEventIds: {},
|
||||
sessionViewId: null,
|
||||
show: true,
|
||||
showCheckboxes: false,
|
||||
sort: [
|
||||
|
|
|
@ -44,6 +44,7 @@ import {
|
|||
updateTimeline,
|
||||
updateTimelineGraphEventId,
|
||||
updateTitleAndDescription,
|
||||
updateTimelineSessionViewSessionId,
|
||||
toggleModalSaveTimeline,
|
||||
updateEqlOptions,
|
||||
setTimelineUpdatedAt,
|
||||
|
@ -77,6 +78,7 @@ import {
|
|||
updateGraphEventId,
|
||||
updateFilters,
|
||||
updateTimelineEventType,
|
||||
updateSessionViewSessionId,
|
||||
} from './helpers';
|
||||
|
||||
import { TimelineState, EMPTY_TIMELINE_BY_ID } from './types';
|
||||
|
@ -146,6 +148,10 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
|
|||
...state,
|
||||
timelineById: updateGraphEventId({ id, graphEventId, timelineById: state.timelineById }),
|
||||
}))
|
||||
.case(updateTimelineSessionViewSessionId, (state, { id, eventId }) => ({
|
||||
...state,
|
||||
timelineById: updateSessionViewSessionId({ id, eventId, timelineById: state.timelineById }),
|
||||
}))
|
||||
.case(pinEvent, (state, { id, eventId }) => ({
|
||||
...state,
|
||||
timelineById: pinTimelineEvent({ id, eventId, timelineById: state.timelineById }),
|
||||
|
|
|
@ -25,7 +25,7 @@ import type {
|
|||
import type { CasesUiStart } from '../../cases/public';
|
||||
import type { SecurityPluginSetup } from '../../security/public';
|
||||
import type { TimelinesUIStart } from '../../timelines/public';
|
||||
import type { SessionViewUIStart } from '../../session_view/public';
|
||||
import type { SessionViewStart } from '../../session_view/public';
|
||||
import type { ResolverPluginSetup } from './resolver/types';
|
||||
import type { Inspect } from '../common/search_strategy';
|
||||
import type { MlPluginSetup, MlPluginStart } from '../../ml/public';
|
||||
|
@ -66,12 +66,12 @@ export interface StartPlugins {
|
|||
newsfeed?: NewsfeedPublicPluginStart;
|
||||
triggersActionsUi: TriggersActionsStart;
|
||||
timelines: TimelinesUIStart;
|
||||
sessionView: SessionViewStart;
|
||||
uiActions: UiActionsStart;
|
||||
ml?: MlPluginStart;
|
||||
spaces?: SpacesPluginStart;
|
||||
dataViewFieldEditor: IndexPatternFieldEditorStart;
|
||||
osquery?: OsqueryPluginStart;
|
||||
sessionView: SessionViewUIStart;
|
||||
}
|
||||
|
||||
export type StartServices = CoreStart &
|
||||
|
|
|
@ -82,7 +82,6 @@ export const buildProcessTree = (
|
|||
events.forEach((event) => {
|
||||
const process = processMap[event.process.entity_id];
|
||||
const parentProcess = processMap[event.process.parent?.entity_id];
|
||||
|
||||
// if session leader, or process already has a parent, return
|
||||
if (process.id === sessionEntityId || process.parent) {
|
||||
return;
|
||||
|
@ -110,12 +109,14 @@ export const buildProcessTree = (
|
|||
|
||||
// with this new page of events processed, lets try re-parent any orphans
|
||||
orphans?.forEach((process) => {
|
||||
const parentProcess = processMap[process.getDetails().process.parent.entity_id];
|
||||
const parentProcessId = process.getDetails().process.parent?.entity_id;
|
||||
|
||||
if (parentProcess) {
|
||||
if (parentProcessId) {
|
||||
const parentProcess = processMap[parentProcessId];
|
||||
process.parent = parentProcess; // handy for recursive operations (like auto expand)
|
||||
|
||||
parentProcess.children.push(process);
|
||||
if (parentProcess !== undefined) {
|
||||
parentProcess.children.push(process);
|
||||
}
|
||||
} else {
|
||||
newOrphans.push(process);
|
||||
}
|
||||
|
|
|
@ -152,11 +152,11 @@ export class ProcessImpl implements Process {
|
|||
group_leader: groupLeader,
|
||||
} = event.process;
|
||||
|
||||
const parentIsASessionLeader = parent.pid === sessionLeader.pid; // possibly bash, zsh or some other shell
|
||||
const processIsAGroupLeader = pid === groupLeader.pid;
|
||||
const parentIsASessionLeader = parent && sessionLeader && parent.pid === sessionLeader.pid;
|
||||
const processIsAGroupLeader = groupLeader && pid === groupLeader.pid;
|
||||
const sessionIsInteractive = !!tty;
|
||||
|
||||
return sessionIsInteractive && parentIsASessionLeader && processIsAGroupLeader;
|
||||
return !!(sessionIsInteractive && parentIsASessionLeader && processIsAGroupLeader);
|
||||
}
|
||||
|
||||
getMaxAlertLevel() {
|
||||
|
@ -184,6 +184,7 @@ export class ProcessImpl implements Process {
|
|||
// to be used as a source for the most up to date details
|
||||
// on the processes lifecycle.
|
||||
getDetailsMemo = memoizeOne((events: ProcessEvent[]) => {
|
||||
// TODO: add these to generator
|
||||
const actionsToFind = [EventAction.fork, EventAction.exec, EventAction.end];
|
||||
const filtered = events.filter((processEvent) => {
|
||||
return actionsToFind.includes(processEvent.event.action);
|
||||
|
@ -192,7 +193,7 @@ export class ProcessImpl implements Process {
|
|||
// because events is already ordered by @timestamp we take the last event
|
||||
// which could be a fork (w no exec or exit), most recent exec event (there can be multiple), or end event.
|
||||
// If a process has an 'end' event will always be returned (since it is last and includes details like exit_code and end time)
|
||||
return filtered[filtered.length - 1] || ({} as ProcessEvent);
|
||||
return filtered[filtered.length - 1];
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ export const useStyles = () => {
|
|||
maxWidth: 800,
|
||||
maxHeight: 378,
|
||||
overflowY: 'auto',
|
||||
backgroundColor: 'white',
|
||||
backgroundColor: colors.emptyShade,
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
|
@ -205,7 +205,7 @@ export function ProcessTreeNode({
|
|||
const shouldRenderChildren = childrenExpanded && children?.length > 0;
|
||||
const childrenTreeDepth = depth + 1;
|
||||
|
||||
const showUserEscalation = user.id !== parent.user.id;
|
||||
const showUserEscalation = user.id && user.id !== parent.user?.id;
|
||||
const interactiveSession = !!tty;
|
||||
const sessionIcon = interactiveSession ? 'consoleApp' : 'compute';
|
||||
const iconTestSubj = hasExec
|
||||
|
|
|
@ -8,11 +8,11 @@ import React, { useState, useCallback, useEffect } from 'react';
|
|||
import {
|
||||
EuiEmptyPrompt,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiResizableContainer,
|
||||
EuiPanel,
|
||||
EuiHorizontalRule,
|
||||
EuiFlexGroup,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { SectionLoading } from '../../shared_imports';
|
||||
|
|
|
@ -11,17 +11,17 @@ import { CSSObject } from '@emotion/react';
|
|||
import { euiLightVars as theme } from '@kbn/ui-theme';
|
||||
|
||||
interface StylesDeps {
|
||||
height: number | undefined;
|
||||
height: string | undefined;
|
||||
}
|
||||
|
||||
export const useStyles = ({ height = 500 }: StylesDeps) => {
|
||||
export const useStyles = ({ height = '500px' }: StylesDeps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const cached = useMemo(() => {
|
||||
const { border } = euiTheme;
|
||||
|
||||
const processTree: CSSObject = {
|
||||
height: `${height}px`,
|
||||
height: `${height}`,
|
||||
position: 'relative',
|
||||
};
|
||||
|
||||
|
@ -34,6 +34,11 @@ export const useStyles = ({ height = 500 }: StylesDeps) => {
|
|||
zIndex: 2,
|
||||
};
|
||||
|
||||
const nonGrowGroup: CSSObject = {
|
||||
display: 'flex',
|
||||
flexGrow: 0,
|
||||
alignItems: 'stretch',
|
||||
};
|
||||
const searchBar: CSSObject = {
|
||||
position: 'relative',
|
||||
margin: `${euiTheme.size.m} ${euiTheme.size.xs} !important`,
|
||||
|
@ -55,6 +60,7 @@ export const useStyles = ({ height = 500 }: StylesDeps) => {
|
|||
return {
|
||||
processTree,
|
||||
detailPanel,
|
||||
nonGrowGroup,
|
||||
resizeHandle,
|
||||
searchBar,
|
||||
buttonsEyeDetail,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { SessionViewPlugin } from './plugin';
|
||||
|
||||
export type { SessionViewUIStart } from './types';
|
||||
export type { SessionViewStart } from './types';
|
||||
|
||||
export function plugin() {
|
||||
return new SessionViewPlugin();
|
||||
|
|
|
@ -4,20 +4,16 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { ReactElement, ReactNode } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { CoreStart } from '../../../../src/core/public';
|
||||
import { ProcessEvent, Teletype } from '../common/types/process_tree';
|
||||
|
||||
export type SessionViewServices = CoreStart;
|
||||
|
||||
export interface SessionViewUIStart {
|
||||
getSessionView: (sessionEntityId: string) => ReactElement;
|
||||
}
|
||||
|
||||
export interface SessionViewDeps {
|
||||
// the root node of the process tree to render. e.g process.entry.entity_id or process.session_leader.entity_id
|
||||
sessionEntityId: string;
|
||||
height?: number;
|
||||
height?: string;
|
||||
// if provided, the session view will jump to and select the provided event if it belongs to the session leader
|
||||
// session view will fetch a page worth of events starting from jumpToEvent as well as a page backwards.
|
||||
jumpToEvent?: ProcessEvent;
|
||||
|
@ -72,3 +68,7 @@ export interface DetailPanelProcessLeader {
|
|||
entryMetaSourceIp: string;
|
||||
executable: string;
|
||||
}
|
||||
|
||||
export interface SessionViewStart {
|
||||
getSessionView: (props: SessionViewDeps) => JSX.Element;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,9 @@ import { Ext } from '../file';
|
|||
export interface ProcessEcs {
|
||||
Ext?: Ext;
|
||||
entity_id?: string[];
|
||||
entry_leader?: ProcessSessionData;
|
||||
session_leader?: ProcessSessionData;
|
||||
group_leader?: ProcessSessionData;
|
||||
exit_code?: number[];
|
||||
hash?: ProcessHashData;
|
||||
parent?: ProcessParentData;
|
||||
|
@ -23,6 +26,12 @@ export interface ProcessEcs {
|
|||
working_directory?: string[];
|
||||
}
|
||||
|
||||
export interface ProcessSessionData {
|
||||
entity_id?: string[];
|
||||
pid?: string[];
|
||||
name?: string[];
|
||||
}
|
||||
|
||||
export interface ProcessHashData {
|
||||
md5?: string[];
|
||||
sha1?: string[];
|
||||
|
|
|
@ -466,6 +466,7 @@ export enum TimelineTabs {
|
|||
graph = 'graph',
|
||||
notes = 'notes',
|
||||
pinned = 'pinned',
|
||||
session = 'session',
|
||||
eql = 'eql',
|
||||
}
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ const StyledEuiPanel = styled(EuiPanel)<{ $isFullScreen: boolean }>`
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
${({ $isFullScreen }) =>
|
||||
$isFullScreen &&
|
||||
|
|
|
@ -218,6 +218,15 @@ export const TIMELINE_EVENTS_FIELDS = [
|
|||
'process.start',
|
||||
'process.title',
|
||||
'process.working_directory',
|
||||
'process.entry_leader.entity_id',
|
||||
'process.entry_leader.name',
|
||||
'process.entry_leader.pid',
|
||||
'process.session_leader.entity_id',
|
||||
'process.session_leader.name',
|
||||
'process.session_leader.pid',
|
||||
'process.group_leader.entity_id',
|
||||
'process.group_leader.name',
|
||||
'process.group_leader.pid',
|
||||
'zeek.session_id',
|
||||
'zeek.connection.local_resp',
|
||||
'zeek.connection.local_orig',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue