mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security Solution][Alert details] - improving session view experience in expandable flyout (#200270)
## Summary This [PR](https://github.com/elastic/kibana/pull/192531) started the move of the analyzer and session view components from the table to the flyout. Shortly after we added an advanced settings (via this [PR](https://github.com/elastic/kibana/pull/194012)) to allow users to switch back and forth between the old table view and the flyout view. This current PR focuses on the session view component and enhances its user experience, when rendered in the expandable flyout. No changes should be made for the user in the table as well as the other usages of the session view component (like for example the Kubernetes dashboard). #### Old UI (in table) https://github.com/user-attachments/assets/015b32fc-69bb-4526-a42d-accad085ad43 ####. New UI (in flyout) https://github.com/user-attachments/assets/9a3eacbf-bf2b-43d4-8e74-ea933ee0d498 As can seen in the video above, when the session view component is opened in the expandable flyout, we show the tree view and the detailed panel separated. This allow for better use of the horizontal space, especially visible on a wide monitor. This is also combined with the fact that the flyout is resizable (and can take the whole screen) and the preview panel is also resizable, to provide more space to the detailed panel. Note: the session view full screen functionality is lost, but this is by design. As mentioned above, the user can resize the flyout's width to take the full screen, and the flyout's vertical space is already near full height. ## Code decisions To guarantee as much as possible that the usage of the Session View component in the table or in the other places (like the Kubernetes dashboard) were not impacted by this PR, only additive changes were made. All these changes are also protected behind `if` conditions, that should only be run when the correct props are being passed in. Some components (like the content of each of the tabs of the detailed panels - Process, Metadata and Alerts) as well as a hook, are exposed outisde of the `session_view` plugin, to be reused in the expandable flyout directly. Code changes were kept to a bare minimum in the `session_view` plugin! ## What to test - functionality of the Session View component should be exactly the same when used in the table as when used in the flyout: - clicking on a row in the tree should update the detailed panel accordingly - jumping to a process from the detailed panel should correctly update the tree - viewing the details of an alert should work - the - the UI will be mostly the same, with some small tweaks: - viewing an alert details now opens a preview panel instead of the flyout. The user can go back to the previous panel by clicking on the `Back` button in the top-left corner - the alerts tab does not show the number of alerts as it previously was. We might be able to get this to work later, but after discussing with Product this is an acceptable solution as the feature is still behind an Advanced Settings - the `Open details` has been replaced by a `expand` icon button, to be more consistent with the rest of the UI in the flyout ### Notes: - there is a small update in the analyzer graph to the icon used in the open detail button. We're now using the `expand` icon to be consistent with the Session View component (which already has another `eye` icon) ## How to test - turn on the `securitySolution:enableVisualizationsInFlyout` Advanced Settings  - generate alerts with data for session view (`yarn test:generate -n http://elastic:changeme@localhost:9200 -k http://elastic:changeme@localhost:5601`) --------- Co-authored-by: Paulo Silva <paulo.henrique@elastic.co>
This commit is contained in:
parent
7de3514828
commit
d4a3c96fd3
24 changed files with 1025 additions and 182 deletions
|
@ -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('<SessionView />', () => {
|
||||
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('<SessionView />', () => {
|
|||
|
||||
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);
|
||||
|
|
|
@ -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 sessionViewConfig = useSessionPreview({ getFieldsData, dataFormattedForFieldBrowser });
|
||||
const { canReadPolicyManagement } = useUserPrivileges().endpointPrivileges;
|
||||
|
||||
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 ? (
|
||||
<div data-test-subj={SESSION_VIEW_TEST_ID}>
|
||||
{sessionView.getSessionView({
|
||||
index,
|
||||
sessionEntityId,
|
||||
sessionStartTime,
|
||||
...sessionViewConfig,
|
||||
isFullScreen: true,
|
||||
loadAlertDetails: openAlertDetailsPreview,
|
||||
openDetailsInExpandableFlyout: (selectedProcess: Process | null) =>
|
||||
openDetailsInPreview(selectedProcess),
|
||||
closeDetailsInExpandableFlyout: () => closeDetailsInPreview(),
|
||||
canReadPolicyManagement,
|
||||
resetJumpToEntityId: jumpToEntityId,
|
||||
resetJumpToCursor: jumpToCursor,
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
|
@ -101,6 +148,6 @@ export const SessionView: FC = () => {
|
|||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
SessionView.displayName = 'SessionView';
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<PanelContentProps> = ({ selectedTabId, tabs }) => {
|
||||
const selectedTabContent = useMemo(() => {
|
||||
return tabs.find((tab) => tab.id === selectedTabId)?.content;
|
||||
}, [selectedTabId, tabs]);
|
||||
|
||||
return <FlyoutBody>{selectedTabContent}</FlyoutBody>;
|
||||
};
|
||||
|
||||
PanelContent.displayName = 'PanelContent';
|
|
@ -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<SessionViewPanelContext | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
export type SessionViewPanelProviderProps = {
|
||||
/**
|
||||
* React components to render
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
} & Partial<SessionViewPanelProps['params']>;
|
||||
|
||||
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 <FlyoutError />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SessionViewPanelContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</SessionViewPanelContext.Provider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
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;
|
||||
};
|
|
@ -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<typeof EuiFlyoutHeader> {
|
||||
/**
|
||||
* 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<PanelHeaderProps> = memo(
|
||||
({ selectedTabId, setSelectedTabId, tabs, ...flyoutHeaderProps }) => {
|
||||
const onSelectedTabChanged = (id: SessionViewPanelPaths) => setSelectedTabId(id);
|
||||
|
||||
const renderTabs = tabs.map((tab, index) => (
|
||||
<EuiTab
|
||||
onClick={() => onSelectedTabChanged(tab.id)}
|
||||
isSelected={tab.id === selectedTabId}
|
||||
key={index}
|
||||
data-test-subj={tab['data-test-subj']}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
));
|
||||
|
||||
return (
|
||||
<FlyoutHeader {...flyoutHeaderProps}>
|
||||
<FlyoutHeaderTabs>{renderTabs}</FlyoutHeaderTabs>
|
||||
</FlyoutHeader>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
PanelHeader.displayName = 'PanelHeader';
|
|
@ -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<Partial<SessionViewPanelProps>> = 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 (
|
||||
<>
|
||||
<PanelHeader
|
||||
tabs={allTabs}
|
||||
selectedTabId={selectedTabId}
|
||||
setSelectedTabId={setSelectedTabId}
|
||||
/>
|
||||
<PanelContent tabs={allTabs} selectedTabId={selectedTabId} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
SessionViewPanel.displayName = 'SessionViewPanel';
|
|
@ -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: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.preview.sessionview.header.processTabLabel"
|
||||
defaultMessage="Process"
|
||||
/>
|
||||
),
|
||||
content: <ProcessTab />,
|
||||
};
|
||||
|
||||
export const metadataTab: SessionViewPanelTabType = {
|
||||
id: 'metadata',
|
||||
'data-test-subj': METADATA_TAB_TEST_ID,
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.preview.sessionview.header.metadataTabLabel"
|
||||
defaultMessage="Metadata"
|
||||
/>
|
||||
),
|
||||
content: <MetadataTab />,
|
||||
};
|
||||
|
||||
export const alertsTab: SessionViewPanelTabType = {
|
||||
id: 'alerts',
|
||||
'data-test-subj': ALERTS_TAB_TEST_ID,
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.preview.sessionview.header.alertsTabLabel"
|
||||
defaultMessage="Alerts"
|
||||
/>
|
||||
),
|
||||
content: <AlertsTab />,
|
||||
};
|
|
@ -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 (
|
||||
<EuiPanel
|
||||
hasBorder={false}
|
||||
hasShadow={false}
|
||||
paddingSize="none"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.securitySolution.flyout.preview.sessionview.alertsContentAriaLabel',
|
||||
{ defaultMessage: 'Process' }
|
||||
)}
|
||||
>
|
||||
<DetailPanelAlertTab
|
||||
alerts={alerts}
|
||||
isFetchingAlerts={isFetchingAlerts}
|
||||
hasNextPageAlerts={hasNextPageAlerts}
|
||||
fetchNextPageAlerts={fetchNextPageAlerts}
|
||||
onJumpToEvent={jumpToEvent}
|
||||
onShowAlertDetails={openAlertDetailsPreview}
|
||||
investigatedAlertId={investigatedAlertId}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
});
|
||||
|
||||
AlertsTab.displayName = 'AlertsTab';
|
|
@ -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 (
|
||||
<EuiPanel
|
||||
hasBorder={false}
|
||||
hasShadow={false}
|
||||
paddingSize="none"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.securitySolution.flyout.preview.sessionview.metadataContentAriaLabel',
|
||||
{ defaultMessage: 'Process' }
|
||||
)}
|
||||
>
|
||||
<DetailPanelMetadataTab
|
||||
processHost={selectedProcess?.details.host}
|
||||
processContainer={selectedProcess?.details.container}
|
||||
processOrchestrator={selectedProcess?.details.orchestrator}
|
||||
processCloud={selectedProcess?.details.cloud}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
});
|
||||
|
||||
MetadataTab.displayName = 'MetadataTab';
|
|
@ -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 (
|
||||
<EuiPanel
|
||||
hasBorder={false}
|
||||
hasShadow={false}
|
||||
paddingSize="none"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.securitySolution.flyout.preview.sessionview.processContentAriaLabel',
|
||||
{ defaultMessage: 'Process' }
|
||||
)}
|
||||
>
|
||||
<DetailPanelProcessTab selectedProcess={process} index={index} />
|
||||
</EuiPanel>
|
||||
);
|
||||
});
|
||||
|
||||
ProcessTab.displayName = 'ProcessTab';
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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<DocumentDetailsProps['params']>;
|
||||
|
||||
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,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -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<SessionViewConfig | null, UseSessionPreviewParams>;
|
||||
describe('useSessionViewConfig', () => {
|
||||
let hookResult: RenderHookResult<SessionViewConfig | null, UseSessionViewConfigParams>;
|
||||
|
||||
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,
|
|
@ -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
|
||||
);
|
|
@ -19,5 +19,7 @@ export interface DocumentDetailsProps extends FlyoutPanelProps {
|
|||
indexName: string;
|
||||
scopeId: string;
|
||||
isPreviewMode?: boolean;
|
||||
jumpToEntityId?: string;
|
||||
jumpToCursor?: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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']
|
|||
<AnalyzerPanel {...(props as AnalyzerPanelExpandableFlyoutProps).params} />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: DocumentDetailsSessionViewPanelKey,
|
||||
component: (props) => (
|
||||
<SessionViewPanelProvider {...(props as SessionViewPanelProps).params}>
|
||||
<SessionViewPanel path={props.path as SessionViewPanelProps['path']} />
|
||||
</SessionViewPanelProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: UserPanelKey,
|
||||
component: (props) => <UserPanel {...(props as UserPanelExpandableFlyoutProps).params} />,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -316,7 +316,12 @@ export function ProcessTreeNode({
|
|||
<Nbsp />
|
||||
<span css={styles.jumpToTop}>
|
||||
<EuiToolTip title={COLLAPSE_ALL}>
|
||||
<EuiButtonIcon size="xs" iconType="fold" onClick={handleCollapseProcessTree} />
|
||||
<EuiButtonIcon
|
||||
size="xs"
|
||||
iconType="fold"
|
||||
onClick={handleCollapseProcessTree}
|
||||
aria-label={COLLAPSE_ALL}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</span>
|
||||
</span>
|
||||
|
|
|
@ -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 ? (
|
||||
<EuiEmptyPrompt
|
||||
iconType="warning"
|
||||
color="danger"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.sessionView.errorHeading"
|
||||
defaultMessage="Error loading Session View"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.sessionView.errorMessage"
|
||||
defaultMessage="There was an error loading the Session View."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
) : null,
|
||||
[hasError]
|
||||
);
|
||||
|
||||
const processTree = useMemo(
|
||||
() =>
|
||||
hasData ? (
|
||||
<div css={styles.processTree}>
|
||||
<ProcessTree
|
||||
key={sessionEntityId + currentJumpToCursor}
|
||||
sessionEntityId={sessionEntityId}
|
||||
data={data.pages}
|
||||
searchQuery={searchQuery}
|
||||
selectedProcess={selectedProcess}
|
||||
onProcessSelected={onProcessSelected}
|
||||
onJumpToOutput={onJumpToOutput}
|
||||
jumpToEntityId={currentJumpToEntityId}
|
||||
investigatedAlertId={investigatedAlertId}
|
||||
isFetching={isFetching}
|
||||
hasPreviousPage={hasPreviousPage}
|
||||
hasNextPage={hasNextPage}
|
||||
fetchNextPage={fetchNextPage}
|
||||
fetchPreviousPage={fetchPreviousPage}
|
||||
setSearchResults={setSearchResults}
|
||||
updatedAlertsStatus={updatedAlertsStatus}
|
||||
onShowAlertDetails={onShowAlertDetails}
|
||||
showTimestamp={displayOptions?.timestamp}
|
||||
verboseMode={displayOptions?.verboseMode}
|
||||
trackEvent={trackEvent}
|
||||
/>
|
||||
</div>
|
||||
) : 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 (
|
||||
<SectionLoading>
|
||||
|
@ -390,103 +520,66 @@ export const SessionView = ({
|
|||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={toggleDetailPanel}
|
||||
iconType="list"
|
||||
data-test-subj="sessionView:sessionViewDetailPanelToggle"
|
||||
fill={!isDetailOpen}
|
||||
>
|
||||
{DETAIL_PANEL}
|
||||
</EuiButton>
|
||||
{openDetailsInExpandableFlyout ? (
|
||||
<EuiButtonIcon onClick={toggleDetailPanelInFlyout} iconType="list" />
|
||||
) : (
|
||||
<EuiButton
|
||||
onClick={toggleDetailPanel}
|
||||
iconType="list"
|
||||
data-test-subj="sessionView:sessionViewDetailPanelToggle"
|
||||
fill={!isDetailOpen}
|
||||
>
|
||||
{DETAIL_PANEL}
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
<EuiResizableContainer>
|
||||
{(EuiResizablePanel, EuiResizableButton, { togglePanel }) => {
|
||||
detailPanelCollapseFn.current = () => {
|
||||
togglePanel?.(sessionViewId, { direction: 'left' });
|
||||
};
|
||||
{openDetailsInExpandableFlyout ? (
|
||||
<>
|
||||
{errorEmptyPrompt}
|
||||
{processTree}
|
||||
</>
|
||||
) : (
|
||||
<EuiResizableContainer>
|
||||
{(EuiResizablePanel, EuiResizableButton, { togglePanel }) => {
|
||||
detailPanelCollapseFn.current = () => {
|
||||
togglePanel?.(sessionViewId, { direction: 'left' });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiResizablePanel initialSize={100} minSize="60%" paddingSize="none">
|
||||
{hasError ? (
|
||||
<EuiEmptyPrompt
|
||||
iconType="warning"
|
||||
color="danger"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.sessionView.errorHeading"
|
||||
defaultMessage="Error loading Session View"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.sessionView.errorMessage"
|
||||
defaultMessage="There was an error loading the Session View."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<EuiResizablePanel initialSize={100} minSize="60%" paddingSize="none">
|
||||
{errorEmptyPrompt}
|
||||
{processTree}
|
||||
</EuiResizablePanel>
|
||||
<EuiResizableButton css={styles.resizeHandle} />
|
||||
<EuiResizablePanel
|
||||
id={sessionViewId}
|
||||
initialSize={30}
|
||||
minSize="320px"
|
||||
paddingSize="none"
|
||||
css={styles.detailPanel}
|
||||
>
|
||||
<SessionViewDetailPanel
|
||||
index={index}
|
||||
alerts={alerts}
|
||||
alertsCount={alertsCount}
|
||||
isFetchingAlerts={isFetchingAlerts}
|
||||
hasNextPageAlerts={hasNextPageAlerts}
|
||||
fetchNextPageAlerts={fetchNextPageAlerts}
|
||||
investigatedAlertId={investigatedAlertId}
|
||||
selectedProcess={selectedProcess}
|
||||
onJumpToEvent={onJumpToEvent}
|
||||
onShowAlertDetails={onShowAlertDetails}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{hasData && (
|
||||
<div css={styles.processTree}>
|
||||
<ProcessTree
|
||||
key={sessionEntityId + currentJumpToCursor}
|
||||
sessionEntityId={sessionEntityId}
|
||||
data={data.pages}
|
||||
searchQuery={searchQuery}
|
||||
selectedProcess={selectedProcess}
|
||||
onProcessSelected={onProcessSelected}
|
||||
onJumpToOutput={onJumpToOutput}
|
||||
jumpToEntityId={currentJumpToEntityId}
|
||||
investigatedAlertId={investigatedAlertId}
|
||||
isFetching={isFetching}
|
||||
hasPreviousPage={hasPreviousPage}
|
||||
hasNextPage={hasNextPage}
|
||||
fetchNextPage={fetchNextPage}
|
||||
fetchPreviousPage={fetchPreviousPage}
|
||||
setSearchResults={setSearchResults}
|
||||
updatedAlertsStatus={updatedAlertsStatus}
|
||||
onShowAlertDetails={onShowAlertDetails}
|
||||
showTimestamp={displayOptions?.timestamp}
|
||||
verboseMode={displayOptions?.verboseMode}
|
||||
trackEvent={trackEvent}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</EuiResizablePanel>
|
||||
|
||||
<EuiResizableButton css={styles.resizeHandle} />
|
||||
<EuiResizablePanel
|
||||
id={sessionViewId}
|
||||
initialSize={30}
|
||||
minSize="320px"
|
||||
paddingSize="none"
|
||||
css={styles.detailPanel}
|
||||
>
|
||||
<SessionViewDetailPanel
|
||||
index={index}
|
||||
alerts={alerts}
|
||||
alertsCount={alertsCount}
|
||||
isFetchingAlerts={isFetchingAlerts}
|
||||
hasNextPageAlerts={hasNextPageAlerts}
|
||||
fetchNextPageAlerts={fetchNextPageAlerts}
|
||||
investigatedAlertId={investigatedAlertId}
|
||||
selectedProcess={selectedProcess}
|
||||
onJumpToEvent={onJumpToEvent}
|
||||
onShowAlertDetails={onShowAlertDetails}
|
||||
/>
|
||||
</EuiResizablePanel>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</EuiResizableContainer>
|
||||
</EuiResizablePanel>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</EuiResizableContainer>
|
||||
)}
|
||||
<TTYPlayer
|
||||
index={index}
|
||||
show={showTTY}
|
||||
|
|
|
@ -13,3 +13,8 @@ export { ENTRY_SESSION_ENTITY_ID_PROPERTY } from '../common';
|
|||
export function plugin() {
|
||||
return new SessionViewPlugin();
|
||||
}
|
||||
|
||||
export { DetailPanelProcessTab } from './components/detail_panel_process_tab';
|
||||
export { DetailPanelMetadataTab } from './components/detail_panel_metadata_tab';
|
||||
export { DetailPanelAlertTab } from './components/detail_panel_alert_tab';
|
||||
export { useFetchSessionViewAlerts } from './components/session_view/hooks';
|
||||
|
|
|
@ -9,6 +9,7 @@ import type {
|
|||
UsageCollectionSetup,
|
||||
UsageCollectionStart,
|
||||
} from '@kbn/usage-collection-plugin/public';
|
||||
import type { Process } from '../common';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface SessionViewPluginSetup {}
|
||||
|
@ -84,6 +85,26 @@ export interface SessionViewDeps {
|
|||
handleOnAlertDetailsClosed: () => 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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue