diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/session_view.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/session_view.test.tsx
index ff3a834225d68..611f59dcfe1f5 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/session_view.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/session_view.test.tsx
@@ -21,12 +21,12 @@ import {
ENTRY_LEADER_ENTITY_ID,
ENTRY_LEADER_START,
} from '../../shared/constants/field_names';
-import { useSessionPreview } from '../../right/hooks/use_session_preview';
+import { useSessionViewConfig } from '../../shared/hooks/use_session_view_config';
import { useSourcererDataView } from '../../../../sourcerer/containers';
import { mockContextValue } from '../../shared/mocks/mock_context';
import { useLicense } from '../../../../common/hooks/use_license';
-jest.mock('../../right/hooks/use_session_preview');
+jest.mock('../../shared/hooks/use_session_view_config');
jest.mock('../../../../common/hooks/use_license');
jest.mock('../../../../sourcerer/containers');
@@ -80,7 +80,7 @@ const renderSessionView = (contextValue: DocumentDetailsContext = mockContextVal
describe('', () => {
beforeEach(() => {
- (useSessionPreview as jest.Mock).mockReturnValue(sessionViewConfig);
+ (useSessionViewConfig as jest.Mock).mockReturnValue(sessionViewConfig);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
jest.mocked(useSourcererDataView).mockReturnValue({
browserFields: {},
@@ -120,7 +120,7 @@ describe('', () => {
it('should render error message and text in header if no sessionConfig', () => {
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
- (useSessionPreview as jest.Mock).mockReturnValue(null);
+ (useSessionViewConfig as jest.Mock).mockReturnValue(null);
const { getByTestId } = renderSessionView();
expect(getByTestId(SESSION_VIEW_NO_DATA_TEST_ID)).toHaveTextContent(NO_DATA_MESSAGE);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/session_view.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/session_view.tsx
index 3b45cd71b0a6f..714eff5d611c0 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/session_view.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/session_view.tsx
@@ -6,27 +6,24 @@
*/
import type { FC } from 'react';
-import React, { useCallback, useMemo } from 'react';
+import React, { memo, useCallback, useMemo } from 'react';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
-import type { TableId } from '@kbn/securitysolution-data-table';
import { EuiPanel } from '@elastic/eui';
-import {
- ANCESTOR_INDEX,
- ENTRY_LEADER_ENTITY_ID,
- ENTRY_LEADER_START,
-} from '../../shared/constants/field_names';
-import { getField } from '../../shared/utils';
+import type { Process } from '@kbn/session-view-plugin/common';
+import type { CustomProcess } from '../../session_view/context';
+import { useUserPrivileges } from '../../../../common/components/user_privileges';
import { SESSION_VIEW_TEST_ID } from './test_ids';
-import { isActiveTimeline } from '../../../../helpers';
import { useSourcererDataView } from '../../../../sourcerer/containers';
-import { DocumentDetailsPreviewPanelKey } from '../../shared/constants/panel_keys';
+import {
+ DocumentDetailsPreviewPanelKey,
+ DocumentDetailsSessionViewPanelKey,
+} from '../../shared/constants/panel_keys';
import { useKibana } from '../../../../common/lib/kibana';
import { useDocumentDetailsContext } from '../../shared/context';
import { SourcererScopeName } from '../../../../sourcerer/store/model';
-import { detectionsTimelineIds } from '../../../../timelines/containers/helpers';
import { ALERT_PREVIEW_BANNER } from '../../preview/constants';
import { useLicense } from '../../../../common/hooks/use_license';
-import { useSessionPreview } from '../../right/hooks/use_session_preview';
+import { useSessionViewConfig } from '../../shared/hooks/use_session_view_config';
import { SessionViewNoDataMessage } from '../../shared/components/session_view_no_data_message';
import { DocumentEventTypes } from '../../../../common/lib/telemetry';
@@ -35,46 +32,47 @@ export const SESSION_VIEW_ID = 'session-view';
/**
* Session view displayed in the document details expandable flyout left section under the Visualize tab
*/
-export const SessionView: FC = () => {
+export const SessionView: FC = memo(() => {
const { sessionView, telemetry } = useKibana().services;
- const { getFieldsData, indexName, scopeId, dataFormattedForFieldBrowser } =
- useDocumentDetailsContext();
+ const {
+ eventId,
+ indexName,
+ getFieldsData,
+ scopeId,
+ dataFormattedForFieldBrowser,
+ jumpToEntityId,
+ jumpToCursor,
+ } = useDocumentDetailsContext();
+
+ const { canReadPolicyManagement } = useUserPrivileges().endpointPrivileges;
- const sessionViewConfig = useSessionPreview({ getFieldsData, dataFormattedForFieldBrowser });
+ const sessionViewConfig = useSessionViewConfig({ getFieldsData, dataFormattedForFieldBrowser });
const isEnterprisePlus = useLicense().isEnterprise();
const isEnabled = sessionViewConfig && isEnterprisePlus;
- const ancestorIndex = getField(getFieldsData(ANCESTOR_INDEX)); // e.g in case of alert, we want to grab it's origin index
- const sessionEntityId = getField(getFieldsData(ENTRY_LEADER_ENTITY_ID)) || '';
- const sessionStartTime = getField(getFieldsData(ENTRY_LEADER_START)) || '';
- const index = ancestorIndex || indexName;
-
- const sourcererScope = useMemo(() => {
- if (isActiveTimeline(scopeId)) {
- return SourcererScopeName.timeline;
- } else if (detectionsTimelineIds.includes(scopeId as TableId)) {
- return SourcererScopeName.detections;
- } else {
- return SourcererScopeName.default;
- }
- }, [scopeId]);
-
- const { selectedPatterns } = useSourcererDataView(sourcererScope);
+ const { selectedPatterns } = useSourcererDataView(SourcererScopeName.detections);
const eventDetailsIndex = useMemo(() => selectedPatterns.join(','), [selectedPatterns]);
- const { openPreviewPanel } = useExpandableFlyoutApi();
+ const { openPreviewPanel, closePreviewPanel } = useExpandableFlyoutApi();
const openAlertDetailsPreview = useCallback(
- (eventId?: string, onClose?: () => void) => {
- openPreviewPanel({
- id: DocumentDetailsPreviewPanelKey,
- params: {
- id: eventId,
- indexName: eventDetailsIndex,
- scopeId,
- banner: ALERT_PREVIEW_BANNER,
- isPreviewMode: true,
- },
- });
+ (evtId?: string, onClose?: () => void) => {
+ // In the SessionView component, when the user clicks on the
+ // expand button to open a alert in the preview panel, this actually also selects the row and opens
+ // the detailed panel in preview.
+ // In order to NOT modify the SessionView code, the setTimeout here guarantees that the alert details preview
+ // will be opened in second, so that we have a correct order in the opened preview panels
+ setTimeout(() => {
+ openPreviewPanel({
+ id: DocumentDetailsPreviewPanelKey,
+ params: {
+ id: evtId,
+ indexName: eventDetailsIndex,
+ scopeId,
+ banner: ALERT_PREVIEW_BANNER,
+ isPreviewMode: true,
+ },
+ });
+ }, 100);
telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, {
location: scopeId,
panel: 'preview',
@@ -83,14 +81,63 @@ export const SessionView: FC = () => {
[openPreviewPanel, eventDetailsIndex, scopeId, telemetry]
);
+ const openDetailsInPreview = useCallback(
+ (selectedProcess: Process | null) => {
+ // We cannot pass the original Process object sent from the SessionView component
+ // as it contains functions (that should not put into Redux)
+ // and also some recursive properties (that will break rison.encode when updating the URL)
+ const simplifiedSelectedProcess: CustomProcess | null = selectedProcess
+ ? {
+ id: selectedProcess.id,
+ details: selectedProcess.getDetails(),
+ endTime: selectedProcess.getEndTime(),
+ }
+ : null;
+
+ openPreviewPanel({
+ id: DocumentDetailsSessionViewPanelKey,
+ params: {
+ eventId,
+ indexName,
+ selectedProcess: simplifiedSelectedProcess,
+ index: sessionViewConfig?.index,
+ sessionEntityId: sessionViewConfig?.sessionEntityId,
+ sessionStartTime: sessionViewConfig?.sessionStartTime,
+ investigatedAlertId: sessionViewConfig?.investigatedAlertId,
+ scopeId,
+ jumpToEntityId,
+ jumpToCursor,
+ },
+ });
+ },
+ [
+ openPreviewPanel,
+ eventId,
+ indexName,
+ sessionViewConfig?.index,
+ sessionViewConfig?.sessionEntityId,
+ sessionViewConfig?.sessionStartTime,
+ sessionViewConfig?.investigatedAlertId,
+ scopeId,
+ jumpToEntityId,
+ jumpToCursor,
+ ]
+ );
+
+ const closeDetailsInPreview = useCallback(() => closePreviewPanel(), [closePreviewPanel]);
+
return isEnabled ? (
{sessionView.getSessionView({
- index,
- sessionEntityId,
- sessionStartTime,
+ ...sessionViewConfig,
isFullScreen: true,
loadAlertDetails: openAlertDetailsPreview,
+ openDetailsInExpandableFlyout: (selectedProcess: Process | null) =>
+ openDetailsInPreview(selectedProcess),
+ closeDetailsInExpandableFlyout: () => closeDetailsInPreview(),
+ canReadPolicyManagement,
+ resetJumpToEntityId: jumpToEntityId,
+ resetJumpToCursor: jumpToCursor,
})}
) : (
@@ -101,6 +148,6 @@ export const SessionView: FC = () => {
/>
);
-};
+});
SessionView.displayName = 'SessionView';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.test.tsx
index 73cf7202b50c1..d27d75aed1e0d 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.test.tsx
@@ -10,7 +10,7 @@ import { TestProviders } from '../../../../common/mock';
import React from 'react';
import { DocumentDetailsContext } from '../../shared/context';
import { SessionPreviewContainer } from './session_preview_container';
-import { useSessionPreview } from '../hooks/use_session_preview';
+import { useSessionViewConfig } from '../../shared/hooks/use_session_view_config';
import { useLicense } from '../../../../common/hooks/use_license';
import { SESSION_PREVIEW_TEST_ID } from './test_ids';
import {
@@ -24,7 +24,7 @@ import { mockContextValue } from '../../shared/mocks/mock_context';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { useInvestigateInTimeline } from '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline';
-jest.mock('../hooks/use_session_preview');
+jest.mock('../../shared/hooks/use_session_view_config');
jest.mock('../../../../common/hooks/use_license');
jest.mock('../../../../common/hooks/use_experimental_features');
jest.mock(
@@ -84,7 +84,7 @@ describe('SessionPreviewContainer', () => {
});
it('should render component and link in header', () => {
- (useSessionPreview as jest.Mock).mockReturnValue(sessionViewConfig);
+ (useSessionViewConfig as jest.Mock).mockReturnValue(sessionViewConfig);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
const { getByTestId } = renderSessionPreview();
@@ -115,7 +115,7 @@ describe('SessionPreviewContainer', () => {
});
it('should render error message and text in header if no sessionConfig', () => {
- (useSessionPreview as jest.Mock).mockReturnValue(null);
+ (useSessionViewConfig as jest.Mock).mockReturnValue(null);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
const { getByTestId, queryByTestId } = renderSessionPreview();
@@ -133,7 +133,7 @@ describe('SessionPreviewContainer', () => {
});
it('should render upsell message in header if no correct license', () => {
- (useSessionPreview as jest.Mock).mockReturnValue(sessionViewConfig);
+ (useSessionViewConfig as jest.Mock).mockReturnValue(sessionViewConfig);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => false });
const { getByTestId, queryByTestId } = renderSessionPreview();
@@ -152,7 +152,7 @@ describe('SessionPreviewContainer', () => {
});
it('should not render link to session viewer if flyout is open in preview', () => {
- (useSessionPreview as jest.Mock).mockReturnValue(sessionViewConfig);
+ (useSessionViewConfig as jest.Mock).mockReturnValue(sessionViewConfig);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
const { getByTestId, queryByTestId } = renderSessionPreview({
@@ -179,7 +179,7 @@ describe('SessionPreviewContainer', () => {
});
it('should not render link to session viewer if flyout is open in preview mode', () => {
- (useSessionPreview as jest.Mock).mockReturnValue(sessionViewConfig);
+ (useSessionViewConfig as jest.Mock).mockReturnValue(sessionViewConfig);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
const { getByTestId, queryByTestId } = renderSessionPreview({
@@ -199,7 +199,7 @@ describe('SessionPreviewContainer', () => {
describe('when visualization in flyout flag is enabled', () => {
it('should open left panel vizualization tab when visualization in flyout flag is on', () => {
mockUseUiSetting.mockReturnValue([true]);
- (useSessionPreview as jest.Mock).mockReturnValue(sessionViewConfig);
+ (useSessionViewConfig as jest.Mock).mockReturnValue(sessionViewConfig);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
const { getByTestId } = renderSessionPreview();
@@ -212,7 +212,7 @@ describe('SessionPreviewContainer', () => {
});
it('should not render link to session viewer if flyout is open in rule preview', () => {
- (useSessionPreview as jest.Mock).mockReturnValue(sessionViewConfig);
+ (useSessionViewConfig as jest.Mock).mockReturnValue(sessionViewConfig);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
const { getByTestId, queryByTestId } = renderSessionPreview({
@@ -230,7 +230,7 @@ describe('SessionPreviewContainer', () => {
});
it('should not render link to session viewer if flyout is open in preview mode', () => {
- (useSessionPreview as jest.Mock).mockReturnValue(sessionViewConfig);
+ (useSessionViewConfig as jest.Mock).mockReturnValue(sessionViewConfig);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
const { getByTestId, queryByTestId } = renderSessionPreview({
@@ -253,7 +253,7 @@ describe('SessionPreviewContainer', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseUiSetting.mockReturnValue([true]);
- (useSessionPreview as jest.Mock).mockReturnValue(sessionViewConfig);
+ (useSessionViewConfig as jest.Mock).mockReturnValue(sessionViewConfig);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true);
});
@@ -304,7 +304,7 @@ describe('SessionPreviewContainer', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseUiSetting.mockReturnValue([false]);
- (useSessionPreview as jest.Mock).mockReturnValue(sessionViewConfig);
+ (useSessionViewConfig as jest.Mock).mockReturnValue(sessionViewConfig);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true);
});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.tsx
index 2b9ffc32b871e..52d6dd134646e 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.tsx
@@ -13,7 +13,7 @@ import { useUiSetting$ } from '@kbn/kibana-react-plugin/public';
import { ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING } from '../../../../../common/constants';
import { useLicense } from '../../../../common/hooks/use_license';
import { SessionPreview } from './session_preview';
-import { useSessionPreview } from '../hooks/use_session_preview';
+import { useSessionViewConfig } from '../../shared/hooks/use_session_view_config';
import { useInvestigateInTimeline } from '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline';
import { useDocumentDetailsContext } from '../../shared/context';
import { ALERTS_ACTIONS } from '../../../../common/lib/apm/user_actions';
@@ -48,7 +48,7 @@ export const SessionPreviewContainer: FC = () => {
);
// decide whether to show the session view or not
- const sessionViewConfig = useSessionPreview({ getFieldsData, dataFormattedForFieldBrowser });
+ const sessionViewConfig = useSessionViewConfig({ getFieldsData, dataFormattedForFieldBrowser });
const isEnterprisePlus = useLicense().isEnterprise();
const isEnabled = sessionViewConfig && isEnterprisePlus;
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/content.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/content.tsx
new file mode 100644
index 0000000000000..ec6ec077f285d
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/content.tsx
@@ -0,0 +1,36 @@
+/*
+ * 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 type { FC } from 'react';
+import React, { useMemo } from 'react';
+import type { SessionViewPanelPaths } from '.';
+import type { SessionViewPanelTabType } from './tabs';
+import { FlyoutBody } from '../../shared/components/flyout_body';
+
+export interface PanelContentProps {
+ /**
+ * Id of the tab selected in the parent component to display its content
+ */
+ selectedTabId: SessionViewPanelPaths;
+ /**
+ * Tabs display right below the flyout's header
+ */
+ tabs: SessionViewPanelTabType[];
+}
+
+/**
+ * SessionView preview panel content, that renders the process, metadata and alerts tab contents.
+ */
+export const PanelContent: FC = ({ selectedTabId, tabs }) => {
+ const selectedTabContent = useMemo(() => {
+ return tabs.find((tab) => tab.id === selectedTabId)?.content;
+ }, [selectedTabId, tabs]);
+
+ return {selectedTabContent};
+};
+
+PanelContent.displayName = 'PanelContent';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/context.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/context.tsx
new file mode 100644
index 0000000000000..6dd2e878957ce
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/context.tsx
@@ -0,0 +1,147 @@
+/*
+ * 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, { createContext, memo, useContext, useMemo } from 'react';
+import type { ProcessEvent } from '@kbn/session-view-plugin/common';
+import { FlyoutError } from '../../shared/components/flyout_error';
+import type { SessionViewPanelProps } from '.';
+
+export interface CustomProcess {
+ /**
+ * Id of the process
+ */
+ id: string;
+ /**
+ * Details of the process (see implementation under getDetailsMemo here: x-pack/plugins/session_view/public/components/process_tree/hooks.ts)
+ */
+ details: ProcessEvent;
+ /**
+ * Timestamp of the 'end' event (see implementation under getEndTime here x-pack/plugins/session_view/public/components/process_tree/hooks.ts)
+ */
+ endTime: string;
+}
+
+export interface SessionViewPanelContext {
+ /**
+ * Id of the document that was initially being investigated in the expandable flyout.
+ * This context needs to store it as it is used within the SessionView preview panel to be able to reopen the left panel with the same document.
+ */
+ eventId: string;
+ /**
+ * Index used when investigating the initial document in the expandable flyout.
+ * This context needs to store it as it is used within the SessionView preview panel to be able to reopen the left panel with the same document.
+ */
+ indexName: string;
+ /**
+ * ScopeId used when investigating the initial document in the expandable flyout.
+ * This context needs to store it as it is used within the SessionView preview panel to be able to reopen the left panel with the same document.
+ */
+ scopeId: string;
+ /**
+ * Store a subset of properties from the SessionView component.
+ * The original object had functions as well as recursive properties, which we should not store in the context.
+ */
+ selectedProcess: CustomProcess | null;
+ /**
+ * index used within the SessionView component
+ */
+ index: string;
+ /**
+ * sessionEntityId value used to correctly render the SessionView component
+ */
+ sessionEntityId: string;
+ /**
+ * sessionStartTime value used to correctly render the SessionView component
+ */
+ sessionStartTime: string;
+ /**
+ * investigatedAlertId value used to correctly render the SessionView component
+ */
+ investigatedAlertId: string;
+}
+
+export const SessionViewPanelContext = createContext(
+ undefined
+);
+
+export type SessionViewPanelProviderProps = {
+ /**
+ * React components to render
+ */
+ children: React.ReactNode;
+} & Partial;
+
+export const SessionViewPanelProvider = memo(
+ ({
+ eventId,
+ indexName,
+ selectedProcess,
+ index,
+ sessionEntityId,
+ sessionStartTime,
+ scopeId,
+ investigatedAlertId,
+ children,
+ }: SessionViewPanelProviderProps) => {
+ const contextValue = useMemo(
+ () =>
+ eventId &&
+ indexName &&
+ selectedProcess &&
+ index &&
+ sessionEntityId &&
+ sessionStartTime &&
+ scopeId &&
+ investigatedAlertId
+ ? {
+ eventId,
+ indexName,
+ selectedProcess,
+ index,
+ sessionEntityId,
+ sessionStartTime,
+ scopeId,
+ investigatedAlertId,
+ }
+ : undefined,
+ [
+ eventId,
+ indexName,
+ selectedProcess,
+ index,
+ sessionEntityId,
+ sessionStartTime,
+ scopeId,
+ investigatedAlertId,
+ ]
+ );
+
+ if (!contextValue) {
+ return ;
+ }
+
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+SessionViewPanelProvider.displayName = 'SessionViewPanelProvider';
+
+export const useSessionViewPanelContext = (): SessionViewPanelContext => {
+ const contextValue = useContext(SessionViewPanelContext);
+
+ if (!contextValue) {
+ throw new Error(
+ 'SessionViewPanelContext can only be used within SessionViewPanelContext provider'
+ );
+ }
+
+ return contextValue;
+};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/header.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/header.tsx
new file mode 100644
index 0000000000000..e1fa7aae991c6
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/header.tsx
@@ -0,0 +1,59 @@
+/*
+ * 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 type { EuiFlyoutHeader } from '@elastic/eui';
+import { EuiTab } from '@elastic/eui';
+import type { FC } from 'react';
+import React, { memo } from 'react';
+import type { SessionViewPanelTabType } from './tabs';
+import type { SessionViewPanelPaths } from '.';
+import { FlyoutHeader } from '../../shared/components/flyout_header';
+import { FlyoutHeaderTabs } from '../../shared/components/flyout_header_tabs';
+
+export interface PanelHeaderProps extends React.ComponentProps {
+ /**
+ * Id of the tab selected in the parent component to display its content
+ */
+ selectedTabId: SessionViewPanelPaths;
+ /**
+ * Callback to set the selected tab id in the parent component
+ * @param selected
+ */
+ setSelectedTabId: (selected: SessionViewPanelPaths) => void;
+ /**
+ * Tabs to display in the header
+ */
+ tabs: SessionViewPanelTabType[];
+}
+
+/**
+ * Renders the process, metadata and alerts tabs in the SessionView preview panel header.
+ */
+export const PanelHeader: FC = memo(
+ ({ selectedTabId, setSelectedTabId, tabs, ...flyoutHeaderProps }) => {
+ const onSelectedTabChanged = (id: SessionViewPanelPaths) => setSelectedTabId(id);
+
+ const renderTabs = tabs.map((tab, index) => (
+ onSelectedTabChanged(tab.id)}
+ isSelected={tab.id === selectedTabId}
+ key={index}
+ data-test-subj={tab['data-test-subj']}
+ >
+ {tab.name}
+
+ ));
+
+ return (
+
+ {renderTabs}
+
+ );
+ }
+);
+
+PanelHeader.displayName = 'PanelHeader';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/index.tsx
new file mode 100644
index 0000000000000..2150c1f25a6c5
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/index.tsx
@@ -0,0 +1,111 @@
+/*
+ * 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 type { FC } from 'react';
+import React, { memo, useCallback, useMemo } from 'react';
+import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
+import { type PanelPath, useExpandableFlyoutApi } from '@kbn/expandable-flyout';
+import { PanelContent } from './content';
+import { PanelHeader } from './header';
+import type { CustomProcess } from './context';
+import { useSessionViewPanelContext } from './context';
+import type { SessionViewPanelTabType } from './tabs';
+import * as tabs from './tabs';
+import { DocumentDetailsSessionViewPanelKey } from '../shared/constants/panel_keys';
+
+export const allTabs = [tabs.processTab, tabs.metadataTab, tabs.alertsTab];
+export type SessionViewPanelPaths = 'process' | 'metadata' | 'alerts';
+
+export interface SessionViewPanelProps extends FlyoutPanelProps {
+ key: typeof DocumentDetailsSessionViewPanelKey;
+ path?: PanelPath;
+ params: {
+ eventId: string;
+ indexName: string;
+ selectedProcess: CustomProcess | null;
+ index: string;
+ sessionEntityId: string;
+ sessionStartTime: string;
+ scopeId: string;
+ investigatedAlertId: string;
+ };
+}
+
+/**
+ * Displays node details panel for session view
+ */
+export const SessionViewPanel: FC> = memo(({ path }) => {
+ const { openPreviewPanel } = useExpandableFlyoutApi();
+ const {
+ eventId,
+ indexName,
+ selectedProcess,
+ index,
+ sessionEntityId,
+ sessionStartTime,
+ scopeId,
+ investigatedAlertId,
+ } = useSessionViewPanelContext();
+
+ const selectedTabId = useMemo(() => {
+ // we use the value passed from the url and use it if it exists in the list of tabs to display
+ if (path) {
+ const selectedTab = allTabs.map((tab) => tab.id).find((tabId) => tabId === path.tab);
+ if (selectedTab) {
+ return selectedTab;
+ }
+ }
+
+ // we default back to the first tab of the list of tabs to display in case everything else has failed
+ return allTabs[0].id;
+ }, [path]);
+
+ const setSelectedTabId = useCallback(
+ (tabId: SessionViewPanelTabType['id']) => {
+ openPreviewPanel({
+ id: DocumentDetailsSessionViewPanelKey,
+ path: {
+ tab: tabId,
+ },
+ params: {
+ eventId,
+ indexName,
+ selectedProcess,
+ index,
+ sessionEntityId,
+ sessionStartTime,
+ scopeId,
+ investigatedAlertId,
+ },
+ });
+ },
+ [
+ eventId,
+ index,
+ indexName,
+ investigatedAlertId,
+ openPreviewPanel,
+ scopeId,
+ selectedProcess,
+ sessionEntityId,
+ sessionStartTime,
+ ]
+ );
+
+ return (
+ <>
+
+
+ >
+ );
+});
+
+SessionViewPanel.displayName = 'SessionViewPanel';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/tabs.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/tabs.tsx
new file mode 100644
index 0000000000000..e536448840837
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/tabs.tsx
@@ -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 type { ReactElement } from 'react';
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { ProcessTab } from './tabs/process_tab';
+import { MetadataTab } from './tabs/metadata_tab';
+import { AlertsTab } from './tabs/alerts_tab';
+import { ALERTS_TAB_TEST_ID, METADATA_TAB_TEST_ID, PROCESS_TAB_TEST_ID } from './test_ids';
+import type { SessionViewPanelPaths } from '.';
+
+export interface SessionViewPanelTabType {
+ id: SessionViewPanelPaths;
+ name: ReactElement;
+ content: React.ReactElement;
+ 'data-test-subj': string;
+}
+
+export const processTab: SessionViewPanelTabType = {
+ id: 'process',
+ 'data-test-subj': PROCESS_TAB_TEST_ID,
+ name: (
+
+ ),
+ content: ,
+};
+
+export const metadataTab: SessionViewPanelTabType = {
+ id: 'metadata',
+ 'data-test-subj': METADATA_TAB_TEST_ID,
+ name: (
+
+ ),
+ content: ,
+};
+
+export const alertsTab: SessionViewPanelTabType = {
+ id: 'alerts',
+ 'data-test-subj': ALERTS_TAB_TEST_ID,
+ name: (
+
+ ),
+ content: ,
+};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/tabs/alerts_tab.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/tabs/alerts_tab.tsx
new file mode 100644
index 0000000000000..7bb55d43c5a0b
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/tabs/alerts_tab.tsx
@@ -0,0 +1,124 @@
+/*
+ * 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, useCallback, useMemo } from 'react';
+import { EuiPanel } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { DetailPanelAlertTab, useFetchSessionViewAlerts } from '@kbn/session-view-plugin/public';
+import type { ProcessEvent } from '@kbn/session-view-plugin/common';
+import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
+import { SESSION_VIEW_ID } from '../../left/components/session_view';
+import {
+ DocumentDetailsLeftPanelKey,
+ DocumentDetailsPreviewPanelKey,
+} from '../../shared/constants/panel_keys';
+import { ALERT_PREVIEW_BANNER } from '../../preview/constants';
+import { useSessionViewPanelContext } from '../context';
+
+/**
+ * Tab displayed in the SessionView preview panel, shows alerts related to the session.
+ */
+export const AlertsTab = memo(() => {
+ const { eventId, indexName, investigatedAlertId, sessionEntityId, sessionStartTime, scopeId } =
+ useSessionViewPanelContext();
+ const {
+ data: alertsData,
+ fetchNextPage: fetchNextPageAlerts,
+ isFetching: isFetchingAlerts,
+ hasNextPage: hasNextPageAlerts,
+ } = useFetchSessionViewAlerts(sessionEntityId, sessionStartTime, undefined);
+
+ // this code mimics what is being done in the x-pack/plugins/session_view/public/components/session_view/index.tsx file
+ const alerts = useMemo(() => {
+ let events: ProcessEvent[] = [];
+
+ if (alertsData) {
+ alertsData.pages.forEach((page) => {
+ events = events.concat(page.events);
+ });
+ }
+
+ return events;
+ }, [alertsData]);
+
+ const { openPreviewPanel, openLeftPanel } = useExpandableFlyoutApi();
+ const openAlertDetailsPreview = useCallback(
+ (evtId?: string, onClose?: () => void) => {
+ openPreviewPanel({
+ id: DocumentDetailsPreviewPanelKey,
+ params: {
+ id: evtId,
+ indexName,
+ scopeId,
+ banner: ALERT_PREVIEW_BANNER,
+ isPreviewMode: true,
+ },
+ });
+ },
+ [openPreviewPanel, indexName, scopeId]
+ );
+
+ // this code mimics what is being done in the x-pack/plugins/session_view/public/components/session_view/index.tsx file
+ const jumpToEvent = useCallback(
+ (event: ProcessEvent) => {
+ let jumpToEntityId = null;
+ let jumpToCursor = null;
+ if (event.process) {
+ const { entity_id: entityId } = event.process;
+ if (entityId !== sessionEntityId) {
+ const alert = event.kibana?.alert;
+ const cursor = alert ? alert?.original_time : event['@timestamp'];
+
+ if (cursor) {
+ jumpToEntityId = entityId;
+ jumpToCursor = cursor;
+ }
+ }
+ }
+
+ openLeftPanel({
+ id: DocumentDetailsLeftPanelKey,
+ params: {
+ id: eventId,
+ indexName,
+ scopeId,
+ jumpToEntityId,
+ jumpToCursor,
+ },
+ path: {
+ tab: 'visualize',
+ subTab: SESSION_VIEW_ID,
+ },
+ });
+ },
+ [eventId, indexName, openLeftPanel, scopeId, sessionEntityId]
+ );
+
+ return (
+
+
+
+ );
+});
+
+AlertsTab.displayName = 'AlertsTab';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/tabs/metadata_tab.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/tabs/metadata_tab.tsx
new file mode 100644
index 0000000000000..2d4486bf7fa68
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/tabs/metadata_tab.tsx
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { memo } from 'react';
+import { EuiPanel } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { DetailPanelMetadataTab } from '@kbn/session-view-plugin/public';
+import { useSessionViewPanelContext } from '../context';
+
+/**
+ * Tab displayed in the SessionView preview panel, shows metadata related process selected in the SessionView tree.
+ */
+export const MetadataTab = memo(() => {
+ const { selectedProcess } = useSessionViewPanelContext();
+
+ return (
+
+
+
+ );
+});
+
+MetadataTab.displayName = 'MetadataTab';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/tabs/process_tab.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/tabs/process_tab.tsx
new file mode 100644
index 0000000000000..b5117a2be0f6f
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/tabs/process_tab.tsx
@@ -0,0 +1,50 @@
+/*
+ * 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, useMemo } from 'react';
+import { EuiPanel } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { DetailPanelProcessTab } from '@kbn/session-view-plugin/public';
+import type { Process } from '@kbn/session-view-plugin/common';
+import { useSessionViewPanelContext } from '../context';
+
+/**
+ * Tab displayed in the SessionView preview panel, shows the details related to the process selected in the SessionView tree.
+ */
+export const ProcessTab = memo(() => {
+ const { selectedProcess, index } = useSessionViewPanelContext();
+
+ // We need to partially recreate the Process object here, as the SessionView code
+ // is expecting a Process object with at least the following properties
+ const process: Process | null = useMemo(
+ () =>
+ selectedProcess
+ ? ({
+ getDetails: () => selectedProcess.details,
+ id: selectedProcess.id,
+ getEndTime: () => selectedProcess.endTime,
+ } as Process)
+ : null,
+ [selectedProcess]
+ );
+
+ return (
+
+
+
+ );
+});
+
+ProcessTab.displayName = 'ProcessTab';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/test_ids.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/test_ids.ts
new file mode 100644
index 0000000000000..c9f19314ef450
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/session_view/test_ids.ts
@@ -0,0 +1,12 @@
+/*
+ * 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 { PREFIX } from '../../shared/test_ids';
+
+export const PROCESS_TAB_TEST_ID = `${PREFIX}ProcessTab` as const;
+export const METADATA_TAB_TEST_ID = `${PREFIX}MetadataTab` as const;
+export const ALERTS_TAB_TEST_ID = `${PREFIX}AlertsTab` as const;
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts
index fa40f1e0e6674..e68313ed6707e 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts
@@ -12,3 +12,4 @@ export const DocumentDetailsPreviewPanelKey = 'document-details-preview' as cons
export const DocumentDetailsIsolateHostPanelKey = 'document-details-isolate-host' as const;
export const DocumentDetailsAlertReasonPanelKey = 'document-details-alert-reason' as const;
export const DocumentDetailsAnalyzerPanelKey = 'document-details-analyzer-details' as const;
+export const DocumentDetailsSessionViewPanelKey = 'document-details-sessions-view-details' as const;
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/context.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/context.tsx
index 12e2ad4f2a0b6..72da35f9286b2 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/context.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/context.tsx
@@ -67,6 +67,14 @@ export interface DocumentDetailsContext {
* Boolean to indicate whether it is a preview panel
*/
isPreviewMode: boolean;
+ /**
+ * To allow communication between the SessionView in the left panel and its preview panels
+ */
+ jumpToEntityId?: string;
+ /**
+ * To allow communication between the SessionView in the left panel and its preview panels
+ */
+ jumpToCursor?: string;
}
/**
@@ -82,7 +90,15 @@ export type DocumentDetailsProviderProps = {
} & Partial;
export const DocumentDetailsProvider = memo(
- ({ id, indexName, scopeId, isPreviewMode, children }: DocumentDetailsProviderProps) => {
+ ({
+ id,
+ indexName,
+ scopeId,
+ jumpToEntityId,
+ jumpToCursor,
+ isPreviewMode,
+ children,
+ }: DocumentDetailsProviderProps) => {
const {
browserFields,
dataAsNestedObject,
@@ -117,20 +133,24 @@ export const DocumentDetailsProvider = memo(
getFieldsData,
isPreview: scopeId === TableId.rulePreview,
isPreviewMode: Boolean(isPreviewMode),
+ jumpToEntityId,
+ jumpToCursor,
}
: undefined,
[
id,
- maybeRule,
indexName,
scopeId,
- browserFields,
dataAsNestedObject,
dataFormattedForFieldBrowser,
searchHit,
+ browserFields,
+ maybeRule?.investigation_fields?.field_names,
refetchFlyoutData,
getFieldsData,
isPreviewMode,
+ jumpToEntityId,
+ jumpToCursor,
]
);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_session_view_config.test.tsx
similarity index 79%
rename from x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx
rename to x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_session_view_config.test.tsx
index 4e2e19c6b54fa..11cb97548e94c 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_session_view_config.test.tsx
@@ -7,15 +7,15 @@
import type { RenderHookResult } from '@testing-library/react';
import { renderHook } from '@testing-library/react';
-import type { UseSessionPreviewParams } from './use_session_preview';
-import { useSessionPreview } from './use_session_preview';
+import type { UseSessionViewConfigParams } from './use_session_view_config';
+import { useSessionViewConfig } from './use_session_view_config';
import type { SessionViewConfig } from '@kbn/securitysolution-data-table/common/types';
-import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data';
-import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser';
-import { mockFieldData, mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data';
+import type { GetFieldsData } from './use_get_fields_data';
+import { mockDataFormattedForFieldBrowser } from '../mocks/mock_data_formatted_for_field_browser';
+import { mockFieldData, mockGetFieldsData } from '../mocks/mock_get_fields_data';
-describe('useSessionPreview', () => {
- let hookResult: RenderHookResult;
+describe('useSessionViewConfig', () => {
+ let hookResult: RenderHookResult;
it(`should return a session view config object if alert ancestor index is available`, () => {
const getFieldsData: GetFieldsData = (field: string) => {
@@ -36,7 +36,7 @@ describe('useSessionPreview', () => {
},
];
- hookResult = renderHook((props: UseSessionPreviewParams) => useSessionPreview(props), {
+ hookResult = renderHook((props: UseSessionViewConfigParams) => useSessionViewConfig(props), {
initialProps: {
getFieldsData,
dataFormattedForFieldBrowser,
@@ -71,7 +71,7 @@ describe('useSessionPreview', () => {
isObjectArray: false,
},
];
- hookResult = renderHook((props: UseSessionPreviewParams) => useSessionPreview(props), {
+ hookResult = renderHook((props: UseSessionViewConfigParams) => useSessionViewConfig(props), {
initialProps: {
getFieldsData: mockGetFieldsData,
dataFormattedForFieldBrowser,
@@ -91,7 +91,7 @@ describe('useSessionPreview', () => {
it(`should return null if data isn't ready for session view`, () => {
const getFieldsData: GetFieldsData = (field: string) => '';
- hookResult = renderHook((props: UseSessionPreviewParams) => useSessionPreview(props), {
+ hookResult = renderHook((props: UseSessionViewConfigParams) => useSessionViewConfig(props), {
initialProps: {
getFieldsData,
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_session_view_config.ts
similarity index 82%
rename from x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts
rename to x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_session_view_config.ts
index 4b2132d265871..891b550c1aee8 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_session_view_config.ts
@@ -7,11 +7,11 @@
import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
import type { SessionViewConfig } from '@kbn/securitysolution-data-table/common/types';
-import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data';
-import { getField } from '../../shared/utils';
-import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data';
+import type { GetFieldsData } from './use_get_fields_data';
+import { getField } from '../utils';
+import { useBasicDataFromDetailsData } from './use_basic_data_from_details_data';
-export interface UseSessionPreviewParams {
+export interface UseSessionViewConfigParams {
/**
* Retrieves searchHit values for the provided field
*/
@@ -25,10 +25,10 @@ export interface UseSessionPreviewParams {
/**
* Hook that returns the session view configuration if the session view is available for the alert
*/
-export const useSessionPreview = ({
+export const useSessionViewConfig = ({
getFieldsData,
dataFormattedForFieldBrowser,
-}: UseSessionPreviewParams): SessionViewConfig | null => {
+}: UseSessionViewConfigParams): SessionViewConfig | null => {
const { indexName: _index, alertId: _id } = useBasicDataFromDetailsData(
dataFormattedForFieldBrowser
);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/types.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/types.tsx
index 00fb1da32449c..bc32e3e5b33bb 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/types.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/types.tsx
@@ -19,5 +19,7 @@ export interface DocumentDetailsProps extends FlyoutPanelProps {
indexName: string;
scopeId: string;
isPreviewMode?: boolean;
+ jumpToEntityId?: string;
+ jumpToCursor?: string;
};
}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/index.tsx
index 64fac23dfa98e..d4a5281e00a4c 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/flyout/index.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/index.tsx
@@ -8,6 +8,9 @@
import React, { memo, useCallback } from 'react';
import { ExpandableFlyout, type ExpandableFlyoutProps } from '@kbn/expandable-flyout';
import { useEuiTheme } from '@elastic/eui';
+import { SessionViewPanelProvider } from './document_details/session_view/context';
+import type { SessionViewPanelProps } from './document_details/session_view';
+import { SessionViewPanel } from './document_details/session_view';
import type { NetworkExpandableFlyoutProps } from './network_details';
import { Flyouts } from './document_details/shared/constants/flyouts';
import {
@@ -17,6 +20,7 @@ import {
DocumentDetailsPreviewPanelKey,
DocumentDetailsAlertReasonPanelKey,
DocumentDetailsAnalyzerPanelKey,
+ DocumentDetailsSessionViewPanelKey,
} from './document_details/shared/constants/panel_keys';
import type { IsolateHostPanelProps } from './document_details/isolate_host';
import { IsolateHostPanel } from './document_details/isolate_host';
@@ -104,6 +108,14 @@ const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels']
),
},
+ {
+ key: DocumentDetailsSessionViewPanelKey,
+ component: (props) => (
+
+
+
+ ),
+ },
{
key: UserPanelKey,
component: (props) => ,
diff --git a/x-pack/solutions/security/plugins/security_solution/public/resolver/view/controls/show_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/resolver/view/controls/show_panel.tsx
index 72fa6c925f680..11626042c79f4 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/resolver/view/controls/show_panel.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/resolver/view/controls/show_panel.tsx
@@ -27,7 +27,7 @@ export const ShowPanelButton = memo(({ showPanelOnClick }: { showPanelOnClick: (
title={showPanelButtonTitle}
aria-label={showPanelButtonTitle}
onClick={showPanelOnClick}
- iconType={'eye'}
+ iconType={'list'}
$backgroundColor={colorMap.graphControlsBackground}
$iconColor={colorMap.graphControls}
$borderColor={colorMap.graphControlsBorderColor}
diff --git a/x-pack/solutions/security/plugins/session_view/public/components/process_tree_node/index.tsx b/x-pack/solutions/security/plugins/session_view/public/components/process_tree_node/index.tsx
index a6201ea2ed504..b493591b94370 100644
--- a/x-pack/solutions/security/plugins/session_view/public/components/process_tree_node/index.tsx
+++ b/x-pack/solutions/security/plugins/session_view/public/components/process_tree_node/index.tsx
@@ -316,7 +316,12 @@ export function ProcessTreeNode({
-
+
diff --git a/x-pack/solutions/security/plugins/session_view/public/components/session_view/index.tsx b/x-pack/solutions/security/plugins/session_view/public/components/session_view/index.tsx
index bdb7e3ddc8c2b..5cf2a444fb92e 100644
--- a/x-pack/solutions/security/plugins/session_view/public/components/session_view/index.tsx
+++ b/x-pack/solutions/security/plugins/session_view/public/components/session_view/index.tsx
@@ -62,6 +62,10 @@ export const SessionView = ({
loadAlertDetails,
canReadPolicyManagement,
trackEvent,
+ openDetailsInExpandableFlyout,
+ closeDetailsInExpandableFlyout,
+ resetJumpToEntityId,
+ resetJumpToCursor,
}: SessionViewDeps & { trackEvent: (name: SessionViewTelemetryKey) => void }) => {
// don't engage jumpTo if jumping to session leader.
if (jumpToEntityId === sessionEntityId) {
@@ -114,9 +118,18 @@ export const SessionView = ({
return !!(!displayOptions?.verboseMode && searchQuery && searchResults?.length === 0);
}, [displayOptions?.verboseMode, searchResults, searchQuery]);
- const onProcessSelected = useCallback((process: Process | null) => {
- setSelectedProcess(process);
- }, []);
+ const onProcessSelected = useCallback(
+ (process: Process | null) => {
+ setSelectedProcess(process);
+
+ // used when SessionView is displayed in the expandable flyout
+ // This refreshes the detailed panel rendered in the flyout preview panel
+ if (openDetailsInExpandableFlyout) {
+ openDetailsInExpandableFlyout(process);
+ }
+ },
+ [openDetailsInExpandableFlyout]
+ );
const onJumpToEvent = useCallback(
(event: ProcessEvent) => {
@@ -182,11 +195,29 @@ export const SessionView = ({
const onToggleTTY = useCallback(() => {
if (hasTTYOutput) {
setShowTTY(!showTTY);
+
+ // used when SessionView is displayed in the expandable flyout
+ // This closes the detailed panel rendered in the flyout preview panel when the user activate the TTY output mode
+ // then reopens the detailed panel to the previously selected process when the user deactivates the TTY output mode
+ if (closeDetailsInExpandableFlyout && !showTTY) {
+ closeDetailsInExpandableFlyout();
+ }
+ if (openDetailsInExpandableFlyout && showTTY) {
+ openDetailsInExpandableFlyout(selectedProcess);
+ }
+
trackEvent('tty_loaded');
} else {
trackEvent('disabled_tty_clicked');
}
- }, [hasTTYOutput, showTTY, trackEvent]);
+ }, [
+ closeDetailsInExpandableFlyout,
+ hasTTYOutput,
+ openDetailsInExpandableFlyout,
+ selectedProcess,
+ showTTY,
+ trackEvent,
+ ]);
const handleRefresh = useCallback(() => {
refetch({ refetchPage: (_page, i, allPages) => allPages.length - 1 === i });
@@ -220,6 +251,19 @@ export const SessionView = ({
fetchAlertStatus[0] ?? ''
);
+ /**
+ * This useEffect should only impact the SessionView component when displayed in the expandable flyout.
+ * The SessionView tree and its detailed panel are separated and this allows the detailed panel to reset the
+ * view of the tree from the preview panel.
+ */
+ useEffect(() => {
+ if (resetJumpToEntityId && resetJumpToCursor) {
+ setSelectedProcess(null);
+ setCurrentJumpToEntityId(resetJumpToEntityId);
+ setCurrentJumpToCursor(resetJumpToCursor);
+ }
+ }, [resetJumpToCursor, resetJumpToEntityId]);
+
useEffect(() => {
if (newUpdatedAlertsStatus) {
setUpdatedAlertsStatus({ ...newUpdatedAlertsStatus });
@@ -261,6 +305,12 @@ export const SessionView = ({
}
}, [isDetailOpen, trackEvent]);
+ const toggleDetailPanelInFlyout = useCallback(() => {
+ if (openDetailsInExpandableFlyout) {
+ openDetailsInExpandableFlyout(selectedProcess);
+ }
+ }, [openDetailsInExpandableFlyout, selectedProcess]);
+
const onShowAlertDetails = useCallback(
(alertUuid: string) => {
if (loadAlertDetails) {
@@ -294,6 +344,86 @@ export const SessionView = ({
[displayOptions?.timestamp, displayOptions?.verboseMode, setDisplayOptions, trackEvent]
);
+ const errorEmptyPrompt = useMemo(
+ () =>
+ hasError ? (
+
+
+
+ }
+ body={
+
+
+
+ }
+ />
+ ) : null,
+ [hasError]
+ );
+
+ const processTree = useMemo(
+ () =>
+ hasData ? (
+
+ ) : null,
+ [
+ currentJumpToCursor,
+ currentJumpToEntityId,
+ data?.pages,
+ displayOptions?.timestamp,
+ displayOptions?.verboseMode,
+ fetchNextPage,
+ fetchPreviousPage,
+ hasData,
+ hasNextPage,
+ hasPreviousPage,
+ investigatedAlertId,
+ isFetching,
+ onJumpToOutput,
+ onProcessSelected,
+ onShowAlertDetails,
+ searchQuery,
+ selectedProcess,
+ sessionEntityId,
+ styles.processTree,
+ trackEvent,
+ updatedAlertsStatus,
+ ]
+ );
+
if (renderIsLoading) {
return (
@@ -390,103 +520,66 @@ export const SessionView = ({
-
- {DETAIL_PANEL}
-
+ {openDetailsInExpandableFlyout ? (
+
+ ) : (
+
+ {DETAIL_PANEL}
+
+ )}
-
- {(EuiResizablePanel, EuiResizableButton, { togglePanel }) => {
- detailPanelCollapseFn.current = () => {
- togglePanel?.(sessionViewId, { direction: 'left' });
- };
-
- return (
- <>
-
- {hasError ? (
-
-
-
- }
- body={
-
-
-
- }
+ {openDetailsInExpandableFlyout ? (
+ <>
+ {errorEmptyPrompt}
+ {processTree}
+ >
+ ) : (
+
+ {(EuiResizablePanel, EuiResizableButton, { togglePanel }) => {
+ detailPanelCollapseFn.current = () => {
+ togglePanel?.(sessionViewId, { direction: 'left' });
+ };
+
+ return (
+ <>
+
+ {errorEmptyPrompt}
+ {processTree}
+
+
+
+
- ) : null}
-
- {hasData && (
-
- )}
-
-
-
-
-
-
- >
- );
- }}
-
+
+ >
+ );
+ }}
+
+ )}
void
) => void;
canReadPolicyManagement?: boolean;
+ /**
+ * Allows to open the detailed panel outside of the SessionView component. This is necessary when the session view is rendered in the
+ * expandable flyout, where the tree and the detailed panel are separated and need to communicate with each other.
+ */
+ openDetailsInExpandableFlyout?: (selectedProcess: Process | null) => void;
+ /**
+ * Allows to close the detailed panel outside of the SessionView component. This is necessary when the session view is rendered in the
+ * expandable flyout: when the user clicks on the TTY output button we need to close the detailed panel.
+ */
+ closeDetailsInExpandableFlyout?: () => void;
+ /**
+ * Allows to reset the view from an external component. This is necessary when the session view is rendered in the
+ * expandable flyout, where the tree and the detailed panels are separated and need to communicate with each other.
+ */
+ resetJumpToEntityId?: string;
+ /**
+ * Allows to reset the view from an external component. This is necessary when the session view is rendered in the
+ * expandable flyout, where the tree and the detailed panels are separated and need to communicate with each other.
+ */
+ resetJumpToCursor?: string;
}
export interface EuiTabProps {