From 2650cd085b1b9a9c60265777d82402d7406ad38a Mon Sep 17 00:00:00 2001 From: christineweng <18648970+christineweng@users.noreply.github.com> Date: Thu, 19 Sep 2024 12:41:00 -0500 Subject: [PATCH] [Security Solution]Analyzer in flyout Part 1 - add split mode in analyzer and enable visualization tab (#192531) ## Summary This PR is part 1 of moving analyzer to alerts flyout. To support rendering the analyzer graph and details panel separately, this PR introduced a `split` mode in analyzer: - In split mode an additional `show panel` button is added in graph control. ![image](https://github.com/user-attachments/assets/5af3d387-69ac-4668-95d4-aedd53e897fb) - default mode shows the panel as part of the graph (alert table scenario) ![image](https://github.com/user-attachments/assets/93a9f0ad-163b-45a3-9e7f-41d83e1cd85b) ## Update to analyzer state in redux To support analyzer in alerts table and in flyout, the redux store for alert/event table uses `scopeId` (`alerts-page` in screenshot), the flyout analyzer uses `flyoutUrl-scopeId` (i.e. `SecuritySolution-alerts-page`) ![image](https://github.com/user-attachments/assets/c7f50866-642a-4e5d-9d80-a303f24934cb) ### Feature flag This feature is currently behind a feature flag `visualizationInFlyoutEnabled` Note: this also enables session view in visualization section ### Video demo https://github.com/user-attachments/assets/6a95497e-1e94-42fe-a227-bbb9a7f0c303 ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../common/experimental_features.ts | 5 + .../public/common/mock/global_state.ts | 2 +- .../analyzer_panels/index.tsx | 39 ++++ .../left/components/analyze_graph.test.tsx | 43 ++++- .../left/components/analyze_graph.tsx | 38 +++- .../flyout/document_details/left/index.tsx | 9 +- .../left/tabs/visualize_tab.tsx | 24 ++- .../analyzer_preview_container.test.tsx | 147 +++++++++++---- .../components/analyzer_preview_container.tsx | 58 +++++- .../visualizations_section.test.tsx | 5 + .../shared/constants/panel_keys.ts | 1 + .../security_solution/public/flyout/index.tsx | 9 + .../test_utilities/simulator/index.tsx | 6 + .../simulator/mock_resolver.tsx | 2 + .../public/resolver/types.ts | 10 + .../resolver/view/controls/index.test.tsx | 62 ++++++ .../public/resolver/view/controls/index.tsx | 14 ++ .../resolver/view/controls/show_panel.tsx | 38 ++++ .../resolver/view/details_panel.test.tsx | 177 ++++++++++++++++++ .../public/resolver/view/details_panel.tsx | 77 ++++++++ .../view/resolver_without_providers.tsx | 16 +- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 24 files changed, 725 insertions(+), 60 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/analyzer_panels/index.tsx create mode 100644 x-pack/plugins/security_solution/public/resolver/view/controls/show_panel.tsx create mode 100644 x-pack/plugins/security_solution/public/resolver/view/details_panel.test.tsx create mode 100644 x-pack/plugins/security_solution/public/resolver/view/details_panel.tsx diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 030e00768349d..58eb49a2a5d9b 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -198,6 +198,11 @@ export const allowedExperimentalValues = Object.freeze({ */ analyzerDatePickersAndSourcererDisabled: false, + /** + * Enables visualization: session viewer and analyzer in expandable flyout + */ + visualizationInFlyoutEnabled: false, + /** * Enables an ability to customize Elastic prebuilt rules. * diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 9201d57b998b8..e3f9f19192a6b 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -445,7 +445,7 @@ export const mockGlobalState: State = { [TableId.test]: EMPTY_RESOLVER, [TimelineId.test]: EMPTY_RESOLVER, [TimelineId.active]: EMPTY_RESOLVER, - flyout: EMPTY_RESOLVER, + [`securitySolution-${TableId.test}`]: EMPTY_RESOLVER, }, sourcerer: { ...mockSourcererState, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/analyzer_panels/index.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/analyzer_panels/index.tsx new file mode 100644 index 0000000000000..cc4be9df60209 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/analyzer_panels/index.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; +import { FlyoutBody } from '@kbn/security-solution-common'; +import type { DocumentDetailsAnalyzerPanelKey } from '../shared/constants/panel_keys'; +import { DetailsPanel } from '../../../resolver/view/details_panel'; + +interface AnalyzerPanelProps extends Record { + /** + * id to identify the scope of analyzer in redux + */ + resolverComponentInstanceID: string; +} + +export interface AnalyzerPanelExpandableFlyoutProps extends FlyoutPanelProps { + key: typeof DocumentDetailsAnalyzerPanelKey; + params: AnalyzerPanelProps; +} + +/** + * Displays node details panel for analyzer + */ +export const AnalyzerPanel: React.FC = ({ resolverComponentInstanceID }) => { + return ( + +
+ +
+
+ ); +}; + +AnalyzerPanel.displayName = 'AnalyzerPanel'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.test.tsx index aff568e29bea8..fc3cb1aba8d36 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.test.tsx @@ -8,17 +8,25 @@ import React from 'react'; import { render } from '@testing-library/react'; import '@testing-library/jest-dom'; +import { TableId } from '@kbn/securitysolution-data-table'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { DocumentDetailsContext } from '../../shared/context'; import { TestProviders } from '../../../../common/mock'; -import { AnalyzeGraph } from './analyze_graph'; +import { AnalyzeGraph, ANALYZER_PREVIEW_BANNER } from './analyze_graph'; import { ANALYZER_GRAPH_TEST_ID } from './test_ids'; +import { useWhichFlyout } from '../../shared/hooks/use_which_flyout'; +import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context'; +import { DocumentDetailsAnalyzerPanelKey } from '../../shared/constants/panel_keys'; jest.mock('react-router-dom', () => { const actual = jest.requireActual('react-router-dom'); return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) }; }); - +jest.mock('@kbn/expandable-flyout'); jest.mock('../../../../resolver/view/use_resolver_query_params_cleaner'); +jest.mock('../../shared/hooks/use_which_flyout'); +const mockUseWhichFlyout = useWhichFlyout as jest.Mock; +const FLYOUT_KEY = 'securitySolution'; const mockDispatch = jest.fn(); jest.mock('react-redux', () => { @@ -31,9 +39,15 @@ jest.mock('react-redux', () => { }); describe('', () => { + beforeEach(() => { + mockUseWhichFlyout.mockReturnValue(FLYOUT_KEY); + jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); + }); + it('renders analyzer graph correctly', () => { const contextValue = { eventId: 'eventId', + scopeId: TableId.test, } as unknown as DocumentDetailsContext; const wrapper = render( @@ -45,4 +59,29 @@ describe('', () => { ); expect(wrapper.getByTestId(ANALYZER_GRAPH_TEST_ID)).toBeInTheDocument(); }); + + it('clicking view button should open details panel in preview', () => { + const contextValue = { + eventId: 'eventId', + scopeId: TableId.test, + } as unknown as DocumentDetailsContext; + + const wrapper = render( + + + + + + ); + + expect(wrapper.getByTestId('resolver:graph-controls:show-panel-button')).toBeInTheDocument(); + wrapper.getByTestId('resolver:graph-controls:show-panel-button').click(); + expect(mockFlyoutApi.openPreviewPanel).toBeCalledWith({ + id: DocumentDetailsAnalyzerPanelKey, + params: { + resolverComponentInstanceID: `${FLYOUT_KEY}-${TableId.test}`, + banner: ANALYZER_PREVIEW_BANNER, + }, + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.tsx index faefd92e9b689..253e51786ff14 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.tsx @@ -6,38 +6,62 @@ */ import type { FC } from 'react'; -import React, { useMemo } from 'react'; - +import React, { useMemo, useCallback } from 'react'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { i18n } from '@kbn/i18n'; +import { useWhichFlyout } from '../../shared/hooks/use_which_flyout'; import { useDocumentDetailsContext } from '../../shared/context'; import { ANALYZER_GRAPH_TEST_ID } from './test_ids'; import { Resolver } from '../../../../resolver/view'; import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; import { isActiveTimeline } from '../../../../helpers'; +import { DocumentDetailsAnalyzerPanelKey } from '../../shared/constants/panel_keys'; export const ANALYZE_GRAPH_ID = 'analyze_graph'; +export const ANALYZER_PREVIEW_BANNER = { + title: i18n.translate( + 'xpack.securitySolution.flyout.left.visualizations.analyzer.panelPreviewTitle', + { + defaultMessage: 'Preview analyzer panels', + } + ), + backgroundColor: 'warning', + textColor: 'warning', +}; + /** * Analyzer graph view displayed in the document details expandable flyout left section under the Visualize tab */ export const AnalyzeGraph: FC = () => { - const { eventId } = useDocumentDetailsContext(); - const scopeId = 'flyout'; // Different scope Id to distinguish flyout and data table analyzers + const { eventId, scopeId } = useDocumentDetailsContext(); + const key = useWhichFlyout() ?? 'memory'; const { from, to, shouldUpdate, selectedPatterns } = useTimelineDataFilters( isActiveTimeline(scopeId) ); const filters = useMemo(() => ({ from, to }), [from, to]); + const { openPreviewPanel } = useExpandableFlyoutApi(); - // TODO as part of https://github.com/elastic/security-team/issues/7032 - // bring back no data message if needed + const onClick = useCallback(() => { + openPreviewPanel({ + id: DocumentDetailsAnalyzerPanelKey, + params: { + resolverComponentInstanceID: `${key}-${scopeId}`, + banner: ANALYZER_PREVIEW_BANNER, + }, + }); + }, [openPreviewPanel, key, scopeId]); return (
); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/index.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/index.tsx index f4a89cbc2b7bc..838209490f7d8 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/index.tsx @@ -38,6 +38,10 @@ export const LeftPanel: FC> = memo(({ path }) => { 'securitySolutionNotesEnabled' ); + const visualizationInFlyoutEnabled = useIsExperimentalFeatureEnabled( + 'visualizationInFlyoutEnabled' + ); + const tabsDisplayed = useMemo(() => { const tabList = eventKind === EventKind.signal @@ -46,8 +50,11 @@ export const LeftPanel: FC> = memo(({ path }) => { if (securitySolutionNotesEnabled && !isPreview) { tabList.push(tabs.notesTab); } + if (visualizationInFlyoutEnabled && !isPreview) { + return [tabs.visualizeTab, ...tabList]; + } return tabList; - }, [eventKind, isPreview, securitySolutionNotesEnabled]); + }, [eventKind, isPreview, securitySolutionNotesEnabled, visualizationInFlyoutEnabled]); const selectedTabId = useMemo(() => { const defaultTab = tabsDisplayed[0].id; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx index 32f64aecd1cb8..031273c3e0892 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx @@ -12,14 +12,22 @@ import { useExpandableFlyoutApi, useExpandableFlyoutState } from '@kbn/expandabl import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { useDocumentDetailsContext } from '../../shared/context'; -import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; +import { useWhichFlyout } from '../../shared/hooks/use_which_flyout'; +import { + DocumentDetailsLeftPanelKey, + DocumentDetailsAnalyzerPanelKey, +} from '../../shared/constants/panel_keys'; import { LeftPanelVisualizeTab } from '..'; import { VISUALIZE_TAB_BUTTON_GROUP_TEST_ID, VISUALIZE_TAB_GRAPH_ANALYZER_BUTTON_TEST_ID, VISUALIZE_TAB_SESSION_VIEW_BUTTON_TEST_ID, } from './test_ids'; -import { ANALYZE_GRAPH_ID, AnalyzeGraph } from '../components/analyze_graph'; +import { + ANALYZE_GRAPH_ID, + AnalyzeGraph, + ANALYZER_PREVIEW_BANNER, +} from '../components/analyze_graph'; import { SESSION_VIEW_ID, SessionView } from '../components/session_view'; import { ALERTS_ACTIONS } from '../../../../common/lib/apm/user_actions'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; @@ -52,11 +60,12 @@ const visualizeButtons: EuiButtonGroupOptionProps[] = [ */ export const VisualizeTab = memo(() => { const { eventId, indexName, scopeId } = useDocumentDetailsContext(); - const { openLeftPanel } = useExpandableFlyoutApi(); + const { openLeftPanel, openPreviewPanel } = useExpandableFlyoutApi(); const panels = useExpandableFlyoutState(); const [activeVisualizationId, setActiveVisualizationId] = useState( panels.left?.path?.subTab ?? SESSION_VIEW_ID ); + const key = useWhichFlyout() ?? 'memory'; const { startTransaction } = useStartTransaction(); const onChangeCompressed = useCallback( (optionId: string) => { @@ -76,8 +85,15 @@ export const VisualizeTab = memo(() => { scopeId, }, }); + openPreviewPanel({ + id: DocumentDetailsAnalyzerPanelKey, + params: { + resolverComponentInstanceID: `${key}-${scopeId}`, + banner: ANALYZER_PREVIEW_BANNER, + }, + }); }, - [startTransaction, eventId, indexName, scopeId, openLeftPanel] + [startTransaction, eventId, indexName, scopeId, openLeftPanel, openPreviewPanel, key] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.test.tsx index 550bda40e5e68..3908aef1eb6b9 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.test.tsx @@ -8,6 +8,7 @@ import { render, screen } from '@testing-library/react'; import { TestProviders } from '../../../../common/mock'; import React from 'react'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { DocumentDetailsContext } from '../../shared/context'; import { mockContextValue } from '../../shared/mocks/mock_context'; import { AnalyzerPreviewContainer } from './analyzer_preview_container'; @@ -22,9 +23,18 @@ import { EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID, EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID, } from '@kbn/security-solution-common'; +import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser'; import { useInvestigateInTimeline } from '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline'; +import { useWhichFlyout } from '../../shared/hooks/use_which_flyout'; +import { + DocumentDetailsLeftPanelKey, + DocumentDetailsAnalyzerPanelKey, +} from '../../shared/constants/panel_keys'; +import { ANALYZE_GRAPH_ID, ANALYZER_PREVIEW_BANNER } from '../../left/components/analyze_graph'; +jest.mock('@kbn/expandable-flyout'); jest.mock( '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver' ); @@ -32,6 +42,10 @@ jest.mock('../../shared/hooks/use_alert_prevalence_from_process_tree'); jest.mock( '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline' ); +jest.mock('../../shared/hooks/use_which_flyout'); +const mockUseWhichFlyout = useWhichFlyout as jest.Mock; +const FLYOUT_KEY = 'securitySolution'; + jest.mock('react-router-dom', () => { const actual = jest.requireActual('react-router-dom'); return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) }; @@ -45,6 +59,9 @@ jest.mock('react-redux', () => { }; }); +jest.mock('../../../../common/hooks/use_experimental_features'); +const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock; + const NO_ANALYZER_MESSAGE = 'You can only visualize events triggered by hosts configured with the Elastic Defend integration or any sysmon data from winlogbeat. Refer to Visual event analyzerExternal link(opens in a new tab or window) for more information.'; @@ -63,8 +80,11 @@ const renderAnalyzerPreview = (context = panelContextValue) => ); describe('AnalyzerPreviewContainer', () => { - afterEach(() => { + beforeEach(() => { jest.clearAllMocks(); + mockUseWhichFlyout.mockReturnValue(FLYOUT_KEY); + jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); + mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); }); it('should render component and link in header', () => { @@ -117,43 +137,104 @@ describe('AnalyzerPreviewContainer', () => { ).toHaveTextContent(NO_ANALYZER_MESSAGE); }); - it('should navigate to analyzer in timeline when clicking on title', () => { - (useIsInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(true); - (useAlertPrevalenceFromProcessTree as jest.Mock).mockReturnValue({ - loading: false, - error: false, - alertIds: ['alertid'], - statsNodes: mock.mockStatsNodes, + describe('when visualizationInFlyoutEnabled is disabled', () => { + it('should navigate to analyzer in timeline when clicking on title', () => { + (useIsInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(true); + (useAlertPrevalenceFromProcessTree as jest.Mock).mockReturnValue({ + loading: false, + error: false, + alertIds: ['alertid'], + statsNodes: mock.mockStatsNodes, + }); + (useInvestigateInTimeline as jest.Mock).mockReturnValue({ + investigateInTimelineAlertClick: jest.fn(), + }); + + const { getByTestId } = renderAnalyzerPreview(); + + const { investigateInTimelineAlertClick } = useInvestigateInTimeline({}); + + getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(ANALYZER_PREVIEW_TEST_ID)).click(); + expect(investigateInTimelineAlertClick).toHaveBeenCalled(); }); - (useInvestigateInTimeline as jest.Mock).mockReturnValue({ - investigateInTimelineAlertClick: jest.fn(), - }); - - const { getByTestId } = renderAnalyzerPreview(); - const { investigateInTimelineAlertClick } = useInvestigateInTimeline({}); - - getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(ANALYZER_PREVIEW_TEST_ID)).click(); - expect(investigateInTimelineAlertClick).toHaveBeenCalled(); + it('should not navigate to analyzer when in preview and clicking on title', () => { + (useIsInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(true); + (useAlertPrevalenceFromProcessTree as jest.Mock).mockReturnValue({ + loading: false, + error: false, + alertIds: ['alertid'], + statsNodes: mock.mockStatsNodes, + }); + (useInvestigateInTimeline as jest.Mock).mockReturnValue({ + investigateInTimelineAlertClick: jest.fn(), + }); + + const { queryByTestId } = renderAnalyzerPreview({ ...panelContextValue, isPreview: true }); + expect( + queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(ANALYZER_PREVIEW_TEST_ID)) + ).not.toBeInTheDocument(); + const { investigateInTimelineAlertClick } = useInvestigateInTimeline({}); + expect(investigateInTimelineAlertClick).not.toHaveBeenCalled(); + }); }); - it('should not navigate to analyzer when in preview and clicking on title', () => { - (useIsInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(true); - (useAlertPrevalenceFromProcessTree as jest.Mock).mockReturnValue({ - loading: false, - error: false, - alertIds: ['alertid'], - statsNodes: mock.mockStatsNodes, - }); - (useInvestigateInTimeline as jest.Mock).mockReturnValue({ - investigateInTimelineAlertClick: jest.fn(), + describe('when visualizationInFlyoutEnabled is enabled', () => { + it('should open left flyout visualization tab when clicking on title', () => { + mockUseIsExperimentalFeatureEnabled.mockReturnValue(true); + + (useIsInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(true); + (useAlertPrevalenceFromProcessTree as jest.Mock).mockReturnValue({ + loading: false, + error: false, + alertIds: ['alertid'], + statsNodes: mock.mockStatsNodes, + }); + (useInvestigateInTimeline as jest.Mock).mockReturnValue({ + investigateInTimelineAlertClick: jest.fn(), + }); + + const { getByTestId } = renderAnalyzerPreview(); + + getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(ANALYZER_PREVIEW_TEST_ID)).click(); + expect(mockFlyoutApi.openLeftPanel).toHaveBeenCalledWith({ + id: DocumentDetailsLeftPanelKey, + path: { + tab: 'visualize', + subTab: ANALYZE_GRAPH_ID, + }, + params: { + id: mockContextValue.eventId, + indexName: mockContextValue.indexName, + scopeId: mockContextValue.scopeId, + }, + }); + expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ + id: DocumentDetailsAnalyzerPanelKey, + params: { + resolverComponentInstanceID: `${FLYOUT_KEY}-${mockContextValue.scopeId}`, + banner: ANALYZER_PREVIEW_BANNER, + }, + }); }); - const { queryByTestId } = renderAnalyzerPreview({ ...panelContextValue, isPreview: true }); - expect( - queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(ANALYZER_PREVIEW_TEST_ID)) - ).not.toBeInTheDocument(); - const { investigateInTimelineAlertClick } = useInvestigateInTimeline({}); - expect(investigateInTimelineAlertClick).not.toHaveBeenCalled(); + it('should not disable link when in rule preview', () => { + mockUseIsExperimentalFeatureEnabled.mockReturnValue(true); + (useIsInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(true); + (useAlertPrevalenceFromProcessTree as jest.Mock).mockReturnValue({ + loading: false, + error: false, + alertIds: ['alertid'], + statsNodes: mock.mockStatsNodes, + }); + (useInvestigateInTimeline as jest.Mock).mockReturnValue({ + investigateInTimelineAlertClick: jest.fn(), + }); + + const { queryByTestId } = renderAnalyzerPreview({ ...panelContextValue, isPreview: true }); + expect( + queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(ANALYZER_PREVIEW_TEST_ID)) + ).not.toBeInTheDocument(); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.tsx index f3d99d9144ad2..ab8b7d8cec668 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.tsx @@ -11,6 +11,14 @@ import { TimelineTabs } from '@kbn/securitysolution-data-table'; import { EuiLink, EuiMark } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { ExpandablePanel } from '@kbn/security-solution-common'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { useWhichFlyout } from '../../shared/hooks/use_which_flyout'; +import { + DocumentDetailsLeftPanelKey, + DocumentDetailsAnalyzerPanelKey, +} from '../../shared/constants/panel_keys'; +import { useKibana } from '../../../../common/lib/kibana'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; import { useInvestigateInTimeline } from '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline'; import { ALERTS_ACTIONS } from '../../../../common/lib/apm/user_actions'; @@ -20,6 +28,7 @@ import { useDocumentDetailsContext } from '../../shared/context'; import { useIsInvestigateInResolverActionEnabled } from '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver'; import { AnalyzerPreview } from './analyzer_preview'; import { ANALYZER_PREVIEW_TEST_ID } from './test_ids'; +import { ANALYZE_GRAPH_ID, ANALYZER_PREVIEW_BANNER } from '../../left/components/analyze_graph'; const timelineId = 'timeline-1'; @@ -27,8 +36,15 @@ const timelineId = 'timeline-1'; * Analyzer preview under Overview, Visualizations. It shows a tree representation of analyzer. */ export const AnalyzerPreviewContainer: React.FC = () => { - const { dataAsNestedObject, isPreview } = useDocumentDetailsContext(); + const { telemetry } = useKibana().services; + const { dataAsNestedObject, isPreview, eventId, indexName, scopeId } = + useDocumentDetailsContext(); + const { openLeftPanel, openPreviewPanel } = useExpandableFlyoutApi(); + const key = useWhichFlyout() ?? 'memory'; + const visualizationInFlyoutEnabled = useIsExperimentalFeatureEnabled( + 'visualizationInFlyoutEnabled' + ); // decide whether to show the analyzer preview or not const isEnabled = useIsInvestigateInResolverActionEnabled(dataAsNestedObject); @@ -54,6 +70,33 @@ export const AnalyzerPreviewContainer: React.FC = () => { dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.graph })); }, [dataAsNestedObject, dispatch, investigateInTimelineAlertClick, startTransaction]); + const gotoVisualizationTab = useCallback(() => { + openLeftPanel({ + id: DocumentDetailsLeftPanelKey, + path: { + tab: 'visualize', + subTab: ANALYZE_GRAPH_ID, + }, + params: { + id: eventId, + indexName, + scopeId, + }, + }); + openPreviewPanel({ + id: DocumentDetailsAnalyzerPanelKey, + params: { + resolverComponentInstanceID: `${key}-${scopeId}`, + banner: ANALYZER_PREVIEW_BANNER, + }, + }); + telemetry.reportDetailsFlyoutTabClicked({ + location: scopeId, + panel: 'left', + tabId: 'visualize', + }); + }, [eventId, indexName, openLeftPanel, openPreviewPanel, key, scopeId, telemetry]); + return ( { defaultMessage="Analyzer preview" /> ), - iconType: 'timeline', + iconType: visualizationInFlyoutEnabled ? 'arrowStart' : 'timeline', ...(isEnabled && !isPreview && { link: { - callback: goToAnalyzerTab, - tooltip: ( + callback: visualizationInFlyoutEnabled ? gotoVisualizationTab : goToAnalyzerTab, + tooltip: visualizationInFlyoutEnabled ? ( + + ) : ( ), diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx index f204c18f9036a..c62824a529e1a 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx @@ -24,6 +24,7 @@ import { TestProvider } from '@kbn/expandable-flyout/src/test/provider'; import { useExpandSection } from '../hooks/use_expand_section'; import { useInvestigateInTimeline } from '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline'; import { useIsInvestigateInResolverActionEnabled } from '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; jest.mock('../hooks/use_expand_section'); jest.mock('../../shared/hooks/use_alert_prevalence_from_process_tree', () => ({ @@ -31,6 +32,9 @@ jest.mock('../../shared/hooks/use_alert_prevalence_from_process_tree', () => ({ })); const mockUseAlertPrevalenceFromProcessTree = useAlertPrevalenceFromProcessTree as jest.Mock; +jest.mock('../../../../common/hooks/use_experimental_features'); +const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock; + jest.mock('../../../../timelines/containers/use_timeline_data_filters', () => ({ useTimelineDataFilters: jest.fn(), })); @@ -68,6 +72,7 @@ const renderVisualizationsSection = (contextValue = panelContextValue) => describe('', () => { beforeEach(() => { + mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); mockUseTimelineDataFilters.mockReturnValue({ selectedPatterns: ['index'] }); mockUseAlertPrevalenceFromProcessTree.mockReturnValue({ loading: false, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts index b3f6cd3343ef1..fa40f1e0e6674 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts @@ -11,3 +11,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; diff --git a/x-pack/plugins/security_solution/public/flyout/index.tsx b/x-pack/plugins/security_solution/public/flyout/index.tsx index 82c82583a4512..ab5c874898ef0 100644 --- a/x-pack/plugins/security_solution/public/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/index.tsx @@ -16,6 +16,7 @@ import { DocumentDetailsRightPanelKey, DocumentDetailsPreviewPanelKey, DocumentDetailsAlertReasonPanelKey, + DocumentDetailsAnalyzerPanelKey, } from './document_details/shared/constants/panel_keys'; import type { IsolateHostPanelProps } from './document_details/isolate_host'; import { IsolateHostPanel } from './document_details/isolate_host'; @@ -39,6 +40,8 @@ import { HostPanel, HostPanelKey, HostPreviewPanelKey } from './entity_details/h import type { HostDetailsExpandableFlyoutProps } from './entity_details/host_details_left'; import { HostDetailsPanel, HostDetailsPanelKey } from './entity_details/host_details_left'; import { NetworkPanel, NetworkPanelKey } from './network_details'; +import type { AnalyzerPanelExpandableFlyoutProps } from './document_details/analyzer_panels'; +import { AnalyzerPanel } from './document_details/analyzer_panels'; /** * List of all panels that will be used within the document details expandable flyout. @@ -95,6 +98,12 @@ const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels'] ), }, + { + key: DocumentDetailsAnalyzerPanelKey, + component: (props) => ( + + ), + }, { key: UserPanelKey, component: (props) => , diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index 7037be2f6189a..3f7f149e8d4f9 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -81,6 +81,8 @@ export class Simulator { history, filters, shouldUpdate, + isSplitPanel, + showPanelOnClick, }: { /** * A (mock) data access layer that will be used to create the Resolver store. @@ -101,6 +103,8 @@ export class Simulator { history?: HistoryPackageHistoryInterface; filters: TimeFilters; shouldUpdate: boolean; + isSplitPanel?: boolean; + showPanelOnClick?: () => void; }) { // create the spy middleware (for debugging tests) this.spyMiddleware = spyMiddlewareFactory(); @@ -152,6 +156,8 @@ export class Simulator { indices={indices} filters={filters} shouldUpdate={shouldUpdate} + isSplitPanel={isSplitPanel} + showPanelOnClick={showPanelOnClick} /> ); } diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx index a37f2185c441b..edefdde302c25 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx @@ -107,6 +107,8 @@ export const MockResolver = React.memo((props: MockResolverProps) => { indices={props.indices} shouldUpdate={props.shouldUpdate} filters={props.filters} + isSplitPanel={props.isSplitPanel} + showPanelOnClick={props.showPanelOnClick} /> diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 60ad9cec4c843..1f18ce35b72ca 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -846,6 +846,16 @@ export interface ResolverProps { * A flag to update data from an external source */ shouldUpdate: boolean; + + /** + * If true, the details panel is not shown in the graph and a view button is shown to manage the panel visibility. + */ + isSplitPanel?: boolean; + + /** + * Optional callback for showing details panels separately from the graph. + */ + showPanelOnClick?: () => void; } /** diff --git a/x-pack/plugins/security_solution/public/resolver/view/controls/index.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/controls/index.test.tsx index fcc6ca2734f85..716a5074c02ad 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/controls/index.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/controls/index.test.tsx @@ -102,6 +102,68 @@ describe('graph controls: when relsover is loaded with an origin node', () => { await expect(originNodeStyle()).toYieldObjectEqualTo(originalPositionStyle); }); + it('should not display the view button by default', async () => { + await expect( + simulator.map(() => ({ + showPanelButton: simulator.testSubject('resolver:graph-controls:show-panel-button').length, + })) + ).toYieldEqualTo({ showPanelButton: 0 }); + }); + + describe('when split mode is enabled', () => { + it('should display the view button when callback is available', async () => { + const { + metadata: { databaseDocumentID }, + dataAccessLayer, + } = noAncestorsTwoChildren(); + + const showPanelOnClick = jest.fn(); + simulator = new Simulator({ + dataAccessLayer, + databaseDocumentID, + resolverComponentInstanceID, + indices: [], + shouldUpdate: false, + filters: {}, + isSplitPanel: true, + showPanelOnClick, + }); + + await expect( + simulator.map(() => ({ + showPanelButton: simulator.testSubject('resolver:graph-controls:show-panel-button') + .length, + })) + ).toYieldEqualTo({ showPanelButton: 1 }); + (await simulator.resolve('resolver:graph-controls:show-panel-button'))?.simulate('click'); + expect(showPanelOnClick).toHaveBeenCalled(); + }); + + it('should not display the view button when callback is not available', async () => { + const { + metadata: { databaseDocumentID }, + dataAccessLayer, + } = noAncestorsTwoChildren(); + + simulator = new Simulator({ + dataAccessLayer, + databaseDocumentID, + resolverComponentInstanceID, + indices: [], + shouldUpdate: false, + filters: {}, + isSplitPanel: true, + }); + + await expect( + simulator.map(() => ({ + showPanelButton: simulator.testSubject('resolver:graph-controls:show-panel-button') + .length, + })) + ).toYieldEqualTo({ showPanelButton: 0 }); + }); + }); + describe('when the user clicks the west panning button', () => { beforeEach(async () => { (await simulator.resolve('resolver:graph-controls:west-button'))?.simulate('click'); diff --git a/x-pack/plugins/security_solution/public/resolver/view/controls/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/controls/index.tsx index 5ab8982a82049..58146c2c8ce1c 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/controls/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/controls/index.tsx @@ -28,6 +28,7 @@ import { DateSelectionButton } from './date_picker'; import { StyledGraphControls, StyledGraphControlsColumn, StyledEuiRange } from './styles'; import { NodeLegend } from './legend'; import { SchemaInformation } from './schema'; +import { ShowPanelButton } from './show_panel'; type PopoverType = null | 'schemaInfo' | 'nodeLegend' | 'sourcererSelection' | 'datePicker'; @@ -35,6 +36,8 @@ export const GraphControls = React.memo( ({ id, className, + isSplitPanel, + showPanelOnClick, }: { /** * Id that identify the scope of analyzer @@ -44,6 +47,14 @@ export const GraphControls = React.memo( * A className string provided by `styled` */ className?: string; + /** + * Indicate if the panel is displayed separately + */ + isSplitPanel?: boolean; + /** + * Callback for showing the panel when isSplitPanel is true + */ + showPanelOnClick?: () => void; }) => { const dispatch = useDispatch(); const scalingFactor = useSelector((state: State) => @@ -148,6 +159,9 @@ export const GraphControls = React.memo( /> ) : null} + {isSplitPanel && showPanelOnClick && ( + + )} diff --git a/x-pack/plugins/security_solution/public/resolver/view/controls/show_panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/controls/show_panel.tsx new file mode 100644 index 0000000000000..72fa6c925f680 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/controls/show_panel.tsx @@ -0,0 +1,38 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { StyledEuiButtonIcon } from './styles'; +import { useColors } from '../use_colors'; + +const showPanelButtonTitle = i18n.translate( + 'xpack.securitySolution.resolver.graphControls.showPanelButtonTitle', + { + defaultMessage: 'Show details panel', + } +); + +export const ShowPanelButton = memo(({ showPanelOnClick }: { showPanelOnClick: () => void }) => { + const colorMap = useColors(); + + return ( + + ); +}); + +ShowPanelButton.displayName = 'ShowPanelButton'; diff --git a/x-pack/plugins/security_solution/public/resolver/view/details_panel.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/details_panel.test.tsx new file mode 100644 index 0000000000000..b0058bb5701ff --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/details_panel.test.tsx @@ -0,0 +1,177 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Provider } from 'react-redux'; +import { render } from '@testing-library/react'; +import { Router } from '@kbn/shared-ux-router'; +import { I18nProvider } from '@kbn/i18n-react'; +import { TestProviders, createMockStore, mockGlobalState } from '../../common/mock'; +import type { ResolverState } from '../types'; +import { createMemoryHistory } from 'history'; +import { coreMock } from '@kbn/core/public/mocks'; +import { DetailsPanel } from './details_panel'; +import { EMPTY_RESOLVER } from '../store/helpers'; +import { uiSetting } from '../mocks/ui_setting'; +import '../test_utilities/extend_jest'; + +const defaultInstanceID = 'details-panel-test'; +const parameters = { databaseDocumentID: 'id', indices: [], agentId: '', filters: {} }; +const schema = { id: 'id', parent: 'parent' }; +const dataSource = 'data source'; + +const renderDetailsPanel = ({ + resolverComponentInstanceID = defaultInstanceID, + reduxState = EMPTY_RESOLVER, +}: { + resolverComponentInstanceID?: string; + reduxState?: ResolverState; +}) => { + // Create a redux store with top level global redux state + const store = createMockStore( + { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + analyzer: { + ...mockGlobalState.sourcerer.sourcererScopes.default, + selectedPatterns: [], + }, + }, + }, + analyzer: { + 'details-panel-test': reduxState, + }, + }, + undefined, + undefined, + undefined + ); + + // If needed, create a fake 'history' instance. + // Resolver will use to read and write query string values. + const history = createMemoryHistory(); + + // Used for `KibanaContextProvider` + const coreStart = coreMock.createStart(); + coreStart.settings.client.get.mockImplementation(uiSetting); + + return render( + + + + + + + + + + ); +}; + +describe('', () => { + describe('When resolver is not initialized', () => { + it('should display a loading state', () => { + const { getByTestId } = renderDetailsPanel({ + resolverComponentInstanceID: 'test', // not in store + }); + expect(getByTestId('resolver:panel:loading')).toBeInTheDocument(); + }); + }); + + describe('When resolver is initialized', () => { + it('should display a loading state when resolver data is loading', () => { + const reduxState: ResolverState = { + ...EMPTY_RESOLVER, + data: { + ...EMPTY_RESOLVER.data, + tree: { + pendingRequestParameters: parameters, + }, + }, + }; + + const { getByTestId } = renderDetailsPanel({ reduxState }); + expect(getByTestId('resolver:panel:loading')).toBeInTheDocument(); + }); + + it("should display error message when entities request doesn't return any data", () => { + const reduxState: ResolverState = { + ...EMPTY_RESOLVER, + data: { + ...EMPTY_RESOLVER.data, + tree: { + ...EMPTY_RESOLVER.data.tree, + lastResponse: { + parameters, + successful: false, + }, + }, + }, + }; + + const { getByTestId, getByText } = renderDetailsPanel({ reduxState }); + expect(getByTestId('resolver:panel:error')).toBeInTheDocument(); + expect(getByText('Error loading data.')).toBeInTheDocument(); + }); + + it("should display a no data message when resolver tree request doesn't return any data", () => { + const reduxState: ResolverState = { + ...EMPTY_RESOLVER, + data: { + ...EMPTY_RESOLVER.data, + tree: { + ...EMPTY_RESOLVER.data.tree, + lastResponse: { + parameters, + schema, + dataSource, + successful: true, + result: { originID: '', nodes: [] }, + }, + }, + }, + }; + + const { getByTestId } = renderDetailsPanel({ reduxState }); + expect(getByTestId('resolver:no-process-events')).toBeInTheDocument(); + }); + + it('should display the node panel when both queries resolve successfaully', () => { + const reduxState: ResolverState = { + ...EMPTY_RESOLVER, + data: { + ...EMPTY_RESOLVER.data, + tree: { + ...EMPTY_RESOLVER.data.tree, + lastResponse: { + parameters, + schema, + dataSource, + successful: true, + result: { + originID: 'id', + nodes: [ + { + id: 'node', + data: { 'host.name': 'test' }, + stats: { total: 1, byCategory: {} }, + }, + ], + }, + }, + }, + }, + }; + + const { getByTestId } = renderDetailsPanel({ reduxState }); + expect(getByTestId('resolver:node-list')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/details_panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/details_panel.tsx new file mode 100644 index 0000000000000..b4158c85c1772 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/details_panel.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useSelector } from 'react-redux'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import * as selectors from '../store/selectors'; +import { PanelRouter } from './panels'; +import { ResolverNoProcessEvents } from './resolver_no_process_events'; +import type { State } from '../../common/store/types'; + +interface DetailsPanelProps { + /** + * Id that identify the scope of analyzer + */ + resolverComponentInstanceID: string; +} + +/** + * Details panel component + */ +const DetailsPanelComponent = React.memo(({ resolverComponentInstanceID }: DetailsPanelProps) => { + const isLoading = useSelector((state: State) => + selectors.isTreeLoading(state.analyzer[resolverComponentInstanceID]) + ); + const hasError = useSelector((state: State) => + selectors.hadErrorLoadingTree(state.analyzer[resolverComponentInstanceID]) + ); + const resolverTreeHasNodes = useSelector((state: State) => + selectors.resolverTreeHasNodes(state.analyzer[resolverComponentInstanceID]) + ); + + return isLoading ? ( +
+ +
+ ) : hasError ? ( +
+
+ {' '} + +
+
+ ) : resolverTreeHasNodes ? ( + + ) : ( + + ); +}); +DetailsPanelComponent.displayName = 'DetailsPanelComponent'; + +/** + * Stand alone details panel to be used when in split panel mode + */ +export const DetailsPanel = React.memo(({ resolverComponentInstanceID }: DetailsPanelProps) => { + const isAnalyzerInitialized = useSelector((state: State) => + Boolean(state.analyzer[resolverComponentInstanceID]) + ); + + return isAnalyzerInitialized ? ( + + ) : ( +
+ +
+ ); +}); + +DetailsPanel.displayName = 'DetailsPanel'; diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx index f9dafcf717ad3..e9090dd7fa9df 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx @@ -42,6 +42,8 @@ export const ResolverWithoutProviders = React.memo( indices, shouldUpdate, filters, + isSplitPanel = false, + showPanelOnClick, }: ResolverProps, refToForward ) { @@ -162,14 +164,20 @@ export const ResolverWithoutProviders = React.memo( ); })} - - - + {!isSplitPanel && ( + + + + )} ) : ( )} - + ); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 8960e3cb69dad..f291729bd6fad 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -39079,7 +39079,6 @@ "xpack.securitySolution.flyout.right.title.otherEventTitle": "Détails de {eventKind}", "xpack.securitySolution.flyout.right.user.userPreviewTitle": "Prévisualiser l'utilisateur", "xpack.securitySolution.flyout.right.visualizations.analyzerPreview.analyzerPreviewTitle": "Aperçu de l'analyseur", - "xpack.securitySolution.flyout.right.visualizations.analyzerPreview.analyzerPreviewTooltip": "Investiguer dans la chronologie", "xpack.securitySolution.flyout.right.visualizations.analyzerPreview.errorDescription": "Une erreur empêche l'analyse de cette alerte.", "xpack.securitySolution.flyout.right.visualizations.analyzerPreview.loadingAriaLabel": "aperçu de l'analyseur", "xpack.securitySolution.flyout.right.visualizations.analyzerPreview.noDataDescription": "Vous pouvez uniquement visualiser les événements déclenchés par les hôtes configurés avec l'intégration Elastic Defend ou les données {sysmon} provenant de {winlogbeat}. Pour en savoir plus, consultez {link}.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e82ea9455271f..786967f137bfe 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -39061,7 +39061,6 @@ "xpack.securitySolution.flyout.right.title.otherEventTitle": "{eventKind}詳細", "xpack.securitySolution.flyout.right.user.userPreviewTitle": "ユーザーをプレビュー", "xpack.securitySolution.flyout.right.visualizations.analyzerPreview.analyzerPreviewTitle": "アナライザープレビュー", - "xpack.securitySolution.flyout.right.visualizations.analyzerPreview.analyzerPreviewTooltip": "タイムラインで調査", "xpack.securitySolution.flyout.right.visualizations.analyzerPreview.errorDescription": "エラーが発生したため、このアラートを分析できません。", "xpack.securitySolution.flyout.right.visualizations.analyzerPreview.loadingAriaLabel": "アナライザープレビュー", "xpack.securitySolution.flyout.right.visualizations.analyzerPreview.noDataDescription": "Elastic Defend統合で構成されたホストまたは{winlogbeat}の{sysmon}データによってトリガーされたイベントのみを可視化できます。詳細については、{link}を参照してください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6933773e814d3..22146249393c6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -39104,7 +39104,6 @@ "xpack.securitySolution.flyout.right.title.otherEventTitle": "{eventKind} 详情", "xpack.securitySolution.flyout.right.user.userPreviewTitle": "预览用户", "xpack.securitySolution.flyout.right.visualizations.analyzerPreview.analyzerPreviewTitle": "分析器预览", - "xpack.securitySolution.flyout.right.visualizations.analyzerPreview.analyzerPreviewTooltip": "在时间线中调查", "xpack.securitySolution.flyout.right.visualizations.analyzerPreview.errorDescription": "出现错误,无法分析此告警。", "xpack.securitySolution.flyout.right.visualizations.analyzerPreview.loadingAriaLabel": "分析器预览", "xpack.securitySolution.flyout.right.visualizations.analyzerPreview.noDataDescription": "您只能可视化由使用 Elastic Defend 集成或来自 {winlogbeat} 的任何 {sysmon} 数据配置的主机触发的事件。请参阅 {link} 了解更多信息。",