diff --git a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx index 594629e9f9e27..911c5a6bdba40 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx @@ -9,11 +9,9 @@ import React from 'react'; import { render } from '@testing-library/react'; import { AlertsPreview } from './alerts_preview'; import { TestProviders } from '../../../common/mock/test_providers'; -import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import type { ParsedAlertsData } from '../../../overview/components/detection_response/alerts_by_status/types'; import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; -import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score'; const mockAlertsData: ParsedAlertsData = { open: { @@ -35,18 +33,14 @@ const mockAlertsData: ParsedAlertsData = { // Mock hooks jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'); -jest.mock('../../../entity_analytics/api/hooks/use_risk_score'); -jest.mock('@kbn/expandable-flyout'); describe('AlertsPreview', () => { const mockOpenLeftPanel = jest.fn(); beforeEach(() => { - (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({ data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } }, }); - (useRiskScore as jest.Mock).mockReturnValue({ data: [{ host: { risk: 75 } }] }); (useMisconfigurationPreview as jest.Mock).mockReturnValue({ data: { count: { passed: 1, failed: 1 } }, }); @@ -58,7 +52,11 @@ describe('AlertsPreview', () => { it('renders', () => { const { getByTestId } = render( - + ); @@ -68,7 +66,11 @@ describe('AlertsPreview', () => { it('renders correct alerts number', () => { const { getByTestId } = render( - + ); @@ -78,7 +80,11 @@ describe('AlertsPreview', () => { it('should render the correct number of distribution bar section based on the number of severities', () => { const { queryAllByTestId } = render( - + ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx index 8592ed61abe33..cd5fcc93495a1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { capitalize } from 'lodash'; import type { EuiThemeComputed } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle, useEuiTheme } from '@elastic/eui'; @@ -18,8 +18,11 @@ import type { } from '../../../overview/components/detection_response/alerts_by_status/types'; import { ExpandablePanel } from '../../../flyout/shared/components/expandable_panel'; import { getSeverityColor } from '../../../detections/components/alerts_kpis/severity_level_panel/helpers'; -import { CspInsightLeftPanelSubTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; -import { useNavigateEntityInsight } from '../../hooks/use_entity_insight'; +import type { EntityDetailsPath } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; +import { + CspInsightLeftPanelSubTab, + EntityDetailsLeftPanelTab, +} from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; const AlertsCount = ({ alertsTotal, @@ -58,14 +61,14 @@ const AlertsCount = ({ export const AlertsPreview = ({ alertsData, - field, - value, isPreviewMode, + openDetailsPanel, + isLinkEnabled, }: { alertsData: ParsedAlertsData; - field: 'host.name' | 'user.name'; - value: string; isPreviewMode?: boolean; + openDetailsPanel: (path: EntityDetailsPath) => void; + isLinkEnabled: boolean; }) => { const { euiTheme } = useEuiTheme(); @@ -90,15 +93,16 @@ export const AlertsPreview = ({ const hasNonClosedAlerts = totalAlertsCount > 0; - const { goToEntityInsightTab } = useNavigateEntityInsight({ - field, - value, - queryIdExtension: isPreviewMode ? 'ALERTS_PREVIEW_TRUE' : 'ALERTS_PREVIEW_FALSE', - subTab: CspInsightLeftPanelSubTab.ALERTS, - }); + const goToEntityInsightTab = useCallback(() => { + openDetailsPanel({ + tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS, + subTab: CspInsightLeftPanelSubTab.ALERTS, + }); + }, [openDetailsPanel]); + const link = useMemo( () => - !isPreviewMode + isLinkEnabled ? { callback: goToEntityInsightTab, tooltip: ( @@ -109,7 +113,7 @@ export const AlertsPreview = ({ ), } : undefined, - [isPreviewMode, goToEntityInsightTab] + [isLinkEnabled, goToEntityInsightTab] ); return ( ({ value, field, isPreviewMode, + isLinkEnabled, + openDetailsPanel, }: { value: string; field: 'host.name' | 'user.name'; isPreviewMode?: boolean; + isLinkEnabled: boolean; + openDetailsPanel: (path: EntityDetailsPath) => void; }) => { const { euiTheme } = useEuiTheme(); const insightContent: React.ReactElement[] = []; @@ -55,9 +60,9 @@ export const EntityInsight = ({ <> > @@ -67,14 +72,26 @@ export const EntityInsight = ({ if (showMisconfigurationsPreview) insightContent.push( <> - + > ); if (showVulnerabilitiesPreview) insightContent.push( <> - + > ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.test.tsx index a3c6bcd38d261..2d79ecdb2783f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.test.tsx @@ -10,25 +10,19 @@ import { render } from '@testing-library/react'; import { MisconfigurationsPreview } from './misconfiguration_preview'; import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; -import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score'; -import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { TestProviders } from '../../../common/mock/test_providers'; // Mock hooks jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'); -jest.mock('../../../entity_analytics/api/hooks/use_risk_score'); -jest.mock('@kbn/expandable-flyout'); describe('MisconfigurationsPreview', () => { const mockOpenLeftPanel = jest.fn(); beforeEach(() => { - (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({ data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } }, }); - (useRiskScore as jest.Mock).mockReturnValue({ data: [{ host: { risk: 75 } }] }); (useMisconfigurationPreview as jest.Mock).mockReturnValue({ data: { count: { passed: 1, failed: 1 } }, }); @@ -37,7 +31,12 @@ describe('MisconfigurationsPreview', () => { it('renders', () => { const { getByTestId } = render( - + ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx index c7c1889a5838b..2db803fbcda3a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { css } from '@emotion/react'; import type { EuiThemeComputed } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, useEuiTheme, EuiTitle } from '@elastic/eui'; @@ -20,8 +20,11 @@ import { uiMetricService, } from '@kbn/cloud-security-posture-common/utils/ui_metrics'; import { ExpandablePanel } from '../../../flyout/shared/components/expandable_panel'; -import { CspInsightLeftPanelSubTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; -import { useNavigateEntityInsight } from '../../hooks/use_entity_insight'; +import type { EntityDetailsPath } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; +import { + CspInsightLeftPanelSubTab, + EntityDetailsLeftPanelTab, +} from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; export const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: number) => { if (passedFindingsStats === 0 && failedFindingsStats === 0) return []; @@ -88,10 +91,14 @@ export const MisconfigurationsPreview = ({ value, field, isPreviewMode, + isLinkEnabled, + openDetailsPanel, }: { value: string; field: 'host.name' | 'user.name'; isPreviewMode?: boolean; + isLinkEnabled: boolean; + openDetailsPanel: (path: EntityDetailsPath) => void; }) => { const { hasMisconfigurationFindings, passedFindings, failedFindings } = useHasMisconfigurations( field, @@ -103,15 +110,16 @@ export const MisconfigurationsPreview = ({ }, []); const { euiTheme } = useEuiTheme(); - const { goToEntityInsightTab } = useNavigateEntityInsight({ - field, - value, - queryIdExtension: 'MISCONFIGURATION_PREVIEW', - subTab: CspInsightLeftPanelSubTab.MISCONFIGURATIONS, - }); + const goToEntityInsightTab = useCallback(() => { + openDetailsPanel({ + tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS, + subTab: CspInsightLeftPanelSubTab.MISCONFIGURATIONS, + }); + }, [openDetailsPanel]); + const link = useMemo( () => - !isPreviewMode + isLinkEnabled ? { callback: goToEntityInsightTab, tooltip: ( @@ -122,7 +130,7 @@ export const MisconfigurationsPreview = ({ ), } : undefined, - [isPreviewMode, goToEntityInsightTab] + [isLinkEnabled, goToEntityInsightTab] ); return ( { const mockOpenLeftPanel = jest.fn(); beforeEach(() => { - (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({ data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } }, }); - (useRiskScore as jest.Mock).mockReturnValue({ data: [{ host: { risk: 75 } }] }); (useMisconfigurationPreview as jest.Mock).mockReturnValue({ data: { count: { passed: 1, failed: 1 } }, }); @@ -37,7 +31,12 @@ describe('VulnerabilitiesPreview', () => { it('renders', () => { const { getByTestId } = render( - + ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx index bbdf05b001637..eb5f022eecc95 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { css } from '@emotion/react'; import type { EuiThemeComputed } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, useEuiTheme, EuiTitle } from '@elastic/eui'; @@ -23,8 +23,11 @@ import { } from '@kbn/cloud-security-posture-common/utils/ui_metrics'; import { METRIC_TYPE } from '@kbn/analytics'; import { ExpandablePanel } from '../../../flyout/shared/components/expandable_panel'; -import { CspInsightLeftPanelSubTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; -import { useNavigateEntityInsight } from '../../hooks/use_entity_insight'; +import type { EntityDetailsPath } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; +import { + CspInsightLeftPanelSubTab, + EntityDetailsLeftPanelTab, +} from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; const VulnerabilitiesCount = ({ vulnerabilitiesTotal, @@ -63,10 +66,14 @@ export const VulnerabilitiesPreview = ({ value, field, isPreviewMode, + isLinkEnabled, + openDetailsPanel, }: { value: string; field: 'host.name' | 'user.name'; isPreviewMode?: boolean; + isLinkEnabled: boolean; + openDetailsPanel: (path: EntityDetailsPath) => void; }) => { useEffect(() => { uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, ENTITY_FLYOUT_WITH_VULNERABILITY_PREVIEW); @@ -93,15 +100,16 @@ export const VulnerabilitiesPreview = ({ const { euiTheme } = useEuiTheme(); - const { goToEntityInsightTab } = useNavigateEntityInsight({ - field, - value, - queryIdExtension: 'VULNERABILITIES_PREVIEW', - subTab: CspInsightLeftPanelSubTab.VULNERABILITIES, - }); + const goToEntityInsightTab = useCallback(() => { + openDetailsPanel({ + tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS, + subTab: CspInsightLeftPanelSubTab.VULNERABILITIES, + }); + }, [openDetailsPanel]); + const link = useMemo( () => - !isPreviewMode + isLinkEnabled ? { callback: goToEntityInsightTab, tooltip: ( @@ -112,7 +120,7 @@ export const VulnerabilitiesPreview = ({ ), } : undefined, - [isPreviewMode, goToEntityInsightTab] + [isLinkEnabled, goToEntityInsightTab] ); return ( = () => { riskScoreData={{ ...mockRiskScoreState, data: [] }} queryId={'testQuery'} recalculatingScore={false} + isLinkEnabled /> @@ -34,7 +35,7 @@ export const Default: Story = () => { ); }; -export const PreviewMode: Story = () => { +export const LinkEnabledInPreviewMode: Story = () => { return ( @@ -43,6 +44,8 @@ export const PreviewMode: Story = () => { riskScoreData={{ ...mockRiskScoreState, data: [] }} queryId={'testQuery'} recalculatingScore={false} + openDetailsPanel={() => {}} + isLinkEnabled isPreviewMode /> @@ -50,3 +53,21 @@ export const PreviewMode: Story = () => { ); }; + +export const LinkDisabled: Story = () => { + return ( + + + + {}} + isLinkEnabled={false} + /> + + + + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx index 9cc773df320b0..04f6ec369e302 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx @@ -41,6 +41,7 @@ describe('FlyoutRiskSummary', () => { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} + isLinkEnabled /> ); @@ -63,6 +64,28 @@ describe('FlyoutRiskSummary', () => { (mockHostRiskScoreState.data?.[0].host.risk.category_2_score ?? 0) }` ); + + expect(getByTestId('riskInputsTitleLink')).toBeInTheDocument(); + expect(getByTestId('riskInputsTitleIcon')).toBeInTheDocument(); + }); + + it('renders link without icon when in preview mode', () => { + const { getByTestId, queryByTestId } = render( + + {}} + recalculatingScore={false} + isLinkEnabled + isPreviewMode + /> + + ); + + expect(getByTestId('risk-summary-table')).toBeInTheDocument(); + expect(getByTestId('riskInputsTitleLink')).toBeInTheDocument(); + expect(queryByTestId('riskInputsTitleIcon')).not.toBeInTheDocument(); }); it('renders risk summary table when riskScoreData is empty', () => { @@ -73,6 +96,7 @@ describe('FlyoutRiskSummary', () => { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} + isLinkEnabled /> ); @@ -87,6 +111,7 @@ describe('FlyoutRiskSummary', () => { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} + isLinkEnabled /> ); @@ -94,7 +119,7 @@ describe('FlyoutRiskSummary', () => { expect(queryByTestId('riskInputsTitleLink')).not.toBeInTheDocument(); }); - it('risk summary header does not render expand icon when in preview mode', () => { + it('risk summary header does not render link when link is not enabled', () => { const { queryByTestId } = render( { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} - isPreviewMode + isLinkEnabled={false} /> ); expect(queryByTestId('riskInputsTitleLink')).not.toBeInTheDocument(); - expect(queryByTestId('riskInputsTitleIcon')).not.toBeInTheDocument(); }); it('renders visualization embeddable', () => { @@ -119,6 +143,7 @@ describe('FlyoutRiskSummary', () => { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} + isLinkEnabled /> ); @@ -134,6 +159,7 @@ describe('FlyoutRiskSummary', () => { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} + isLinkEnabled /> ); @@ -149,6 +175,7 @@ describe('FlyoutRiskSummary', () => { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} + isLinkEnabled /> ); @@ -176,6 +203,7 @@ describe('FlyoutRiskSummary', () => { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} + isLinkEnabled /> ); @@ -198,6 +226,7 @@ describe('FlyoutRiskSummary', () => { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} + isLinkEnabled /> ); @@ -220,6 +249,7 @@ describe('FlyoutRiskSummary', () => { queryId={'testQuery'} openDetailsPanel={() => {}} recalculatingScore={false} + isLinkEnabled /> ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx index 0c42543a7f91e..f655f346f93f5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx @@ -23,6 +23,7 @@ import { euiThemeVars } from '@kbn/ui-theme'; import dateMath from '@kbn/datemath'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../common/lib/kibana/kibana_react'; +import type { EntityDetailsPath } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; import { EntityDetailsLeftPanelTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect'; import { ONE_WEEK_IN_HOURS } from '../../../flyout/entity_details/shared/constants'; @@ -49,7 +50,8 @@ export interface RiskSummaryProps { riskScoreData: RiskScoreState; recalculatingScore: boolean; queryId: string; - openDetailsPanel?: (tab: EntityDetailsLeftPanelTab) => void; + openDetailsPanel: (path: EntityDetailsPath) => void; + isLinkEnabled: boolean; isPreviewMode?: boolean; } @@ -58,6 +60,7 @@ const FlyoutRiskSummaryComponent = ({ recalculatingScore, queryId, openDetailsPanel, + isLinkEnabled, isPreviewMode, }: RiskSummaryProps) => { const { telemetry } = useKibana().services; @@ -178,8 +181,8 @@ const FlyoutRiskSummaryComponent = ({ link: riskScoreData.loading ? undefined : { - callback: openDetailsPanel - ? () => openDetailsPanel(EntityDetailsLeftPanelTab.RISK_INPUTS) + callback: isLinkEnabled + ? () => openDetailsPanel({ tab: EntityDetailsLeftPanelTab.RISK_INPUTS }) : undefined, tooltip: ( {}} recalculatingScore={false} + isLinkEnabled={true} /> )) .add('no observed data', () => ( @@ -62,6 +63,7 @@ storiesOf('Components/HostPanelContent', module) hostName={'test-host-name'} onAssetCriticalityChange={() => {}} recalculatingScore={false} + isLinkEnabled={true} /> )) .add('loading', () => ( @@ -87,5 +89,6 @@ storiesOf('Components/HostPanelContent', module) hostName={'test-host-name'} onAssetCriticalityChange={() => {}} recalculatingScore={false} + isLinkEnabled={true} /> )); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx index 9c2ce61dea7fc..5b8746675cfdf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx @@ -17,7 +17,7 @@ import { ObservedEntity } from '../shared/components/observed_entity'; import { HOST_PANEL_OBSERVED_HOST_QUERY_ID, HOST_PANEL_RISK_SCORE_QUERY_ID } from '.'; import type { ObservedEntityData } from '../shared/components/observed_entity/types'; import { useObservedHostFields } from './hooks/use_observed_host_fields'; -import type { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; +import type { EntityDetailsPath } from '../shared/components/left_panel/left_panel_header'; interface HostPanelContentProps { observedHost: ObservedEntityData; @@ -25,11 +25,12 @@ interface HostPanelContentProps { contextID: string; scopeId: string; isDraggable: boolean; - openDetailsPanel?: (tab: EntityDetailsLeftPanelTab) => void; + openDetailsPanel: (path: EntityDetailsPath) => void; hostName: string; onAssetCriticalityChange: () => void; recalculatingScore: boolean; isPreviewMode?: boolean; + isLinkEnabled: boolean; } export const HostPanelContent = ({ @@ -43,6 +44,7 @@ export const HostPanelContent = ({ openDetailsPanel, onAssetCriticalityChange, isPreviewMode, + isLinkEnabled, }: HostPanelContentProps) => { const observedFields = useObservedHostFields(observedHost); @@ -56,6 +58,7 @@ export const HostPanelContent = ({ queryId={HOST_PANEL_RISK_SCORE_QUERY_ID} openDetailsPanel={openDetailsPanel} isPreviewMode={isPreviewMode} + isLinkEnabled={isLinkEnabled} /> > @@ -64,7 +67,13 @@ export const HostPanelContent = ({ entity={{ name: hostName, type: 'host' }} onChange={onAssetCriticalityChange} /> - + { + const original = jest.requireActual('../../../../common/lib/kibana'); + return { + ...original, + useKibana: () => ({ + ...original.useKibana(), + services: { + ...original.useKibana().services, + telemetry: mockedTelemetry, + }, + }), + }; +}); + +const mockProps = { + hostName: 'testHost', + scopeId: 'testScopeId', + isRiskScoreExist: false, + hasMisconfigurationFindings: false, + hasVulnerabilitiesFindings: false, + hasNonClosedAlerts: false, + contextID: 'testContextID', + isPreviewMode: false, +}; + +const tab = EntityDetailsLeftPanelTab.RISK_INPUTS; +const subTab = CspInsightLeftPanelSubTab.MISCONFIGURATIONS; + +const mockOpenLeftPanel = jest.fn(); +const mockOpenFlyout = jest.fn(); + +describe('useNavigateToHostDetails', () => { + describe('when preview navigation is enabled', () => { + beforeEach(() => { + jest.clearAllMocks(); + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ + openLeftPanel: mockOpenLeftPanel, + openFlyout: mockOpenFlyout, + }); + }); + + it('returns callback that opens details panel when not in preview mode', () => { + const { result } = renderHook(() => useNavigateToHostDetails(mockProps)); + + expect(result.current.isLinkEnabled).toBe(true); + result.current.openDetailsPanel({ tab, subTab }); + + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ + id: HostDetailsPanelKey, + params: { + name: mockProps.hostName, + scopeId: mockProps.scopeId, + isRiskScoreExist: mockProps.isRiskScoreExist, + path: { tab, subTab }, + hasMisconfigurationFindings: mockProps.hasMisconfigurationFindings, + hasVulnerabilitiesFindings: mockProps.hasVulnerabilitiesFindings, + hasNonClosedAlerts: mockProps.hasNonClosedAlerts, + }, + }); + expect(mockOpenFlyout).not.toHaveBeenCalled(); + }); + + it('returns callback that opens flyout when in preview mode', () => { + const { result } = renderHook(() => + useNavigateToHostDetails({ ...mockProps, isPreviewMode: true }) + ); + + expect(result.current.isLinkEnabled).toBe(true); + result.current.openDetailsPanel({ tab, subTab }); + + expect(mockOpenFlyout).toHaveBeenCalledWith({ + right: { + id: HostPanelKey, + params: { + contextID: mockProps.contextID, + scopeId: mockProps.scopeId, + hostName: mockProps.hostName, + isDraggable: undefined, + }, + }, + left: { + id: HostDetailsPanelKey, + params: { + name: mockProps.hostName, + scopeId: mockProps.scopeId, + isRiskScoreExist: mockProps.isRiskScoreExist, + path: { tab, subTab }, + hasMisconfigurationFindings: mockProps.hasMisconfigurationFindings, + hasVulnerabilitiesFindings: mockProps.hasVulnerabilitiesFindings, + hasNonClosedAlerts: mockProps.hasNonClosedAlerts, + }, + }, + }); + expect(mockOpenLeftPanel).not.toHaveBeenCalled(); + }); + }); + + describe('when preview navigation is not enabled', () => { + beforeEach(() => { + jest.clearAllMocks(); + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ + openLeftPanel: mockOpenLeftPanel, + openFlyout: mockOpenFlyout, + }); + }); + + it('returns callback that opens details panel when not in preview mode', () => { + const { result } = renderHook(() => useNavigateToHostDetails(mockProps)); + + expect(result.current.isLinkEnabled).toBe(true); + result.current.openDetailsPanel({ tab, subTab }); + + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ + id: HostDetailsPanelKey, + params: { + name: mockProps.hostName, + scopeId: mockProps.scopeId, + isRiskScoreExist: mockProps.isRiskScoreExist, + path: { tab, subTab }, + hasMisconfigurationFindings: mockProps.hasMisconfigurationFindings, + hasVulnerabilitiesFindings: mockProps.hasVulnerabilitiesFindings, + hasNonClosedAlerts: mockProps.hasNonClosedAlerts, + }, + }); + expect(mockOpenFlyout).not.toHaveBeenCalled(); + }); + + it('returns empty callback and isLinkEnabled is false when in preview mode', () => { + const { result } = renderHook(() => + useNavigateToHostDetails({ ...mockProps, isPreviewMode: true }) + ); + + expect(result.current.isLinkEnabled).toBe(false); + result.current.openDetailsPanel({ tab, subTab }); + + expect(mockOpenLeftPanel).not.toHaveBeenCalled(); + expect(mockOpenFlyout).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_navigate_to_host_details.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_navigate_to_host_details.ts new file mode 100644 index 0000000000000..2834446193e8b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_navigate_to_host_details.ts @@ -0,0 +1,110 @@ +/* + * 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 { useCallback, useMemo } from 'react'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useKibana } from '../../../../common/lib/kibana'; +import { HostPanelKey } from '..'; +import { HostDetailsPanelKey } from '../../host_details_left'; +import type { EntityDetailsPath } from '../../shared/components/left_panel/left_panel_header'; +import { EntityEventTypes } from '../../../../common/lib/telemetry'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; + +interface UseNavigateToHostDetailsParams { + hostName: string; + scopeId: string; + isRiskScoreExist: boolean; + hasMisconfigurationFindings: boolean; + hasVulnerabilitiesFindings: boolean; + hasNonClosedAlerts: boolean; + isPreviewMode?: boolean; + contextID: string; + isDraggable?: boolean; +} + +interface UseNavigateToHostDetailsResult { + openDetailsPanel: (path: EntityDetailsPath) => void; + isLinkEnabled: boolean; +} + +export const useNavigateToHostDetails = ({ + hostName, + scopeId, + isRiskScoreExist, + hasMisconfigurationFindings, + hasVulnerabilitiesFindings, + hasNonClosedAlerts, + isPreviewMode, + contextID, + isDraggable, +}: UseNavigateToHostDetailsParams): UseNavigateToHostDetailsResult => { + const { telemetry } = useKibana().services; + const { openLeftPanel, openFlyout } = useExpandableFlyoutApi(); + const isNewNavigationEnabled = useIsExperimentalFeatureEnabled( + 'newExpandableFlyoutNavigationEnabled' + ); + + telemetry.reportEvent(EntityEventTypes.RiskInputsExpandedFlyoutOpened, { + entity: 'host', + }); + + const isLinkEnabled = useMemo(() => { + return !isPreviewMode || (isNewNavigationEnabled && isPreviewMode); + }, [isNewNavigationEnabled, isPreviewMode]); + + const openDetailsPanel = useCallback( + (path?: EntityDetailsPath) => { + const left = { + id: HostDetailsPanelKey, + params: { + name: hostName, + scopeId, + isRiskScoreExist, + path, + hasMisconfigurationFindings, + hasVulnerabilitiesFindings, + hasNonClosedAlerts, + }, + }; + + const right = { + id: HostPanelKey, + params: { + contextID, + scopeId, + hostName, + isDraggable, + }, + }; + + // When new navigation is enabled, nevigation in preview is enabled and open a new flyout + if (isNewNavigationEnabled && isPreviewMode) { + openFlyout({ right, left }); + } + // When not in preview mode, open left panel as usual + else if (!isPreviewMode) { + openLeftPanel(left); + } + }, + [ + isNewNavigationEnabled, + isPreviewMode, + openFlyout, + openLeftPanel, + hostName, + scopeId, + isRiskScoreExist, + hasMisconfigurationFindings, + hasVulnerabilitiesFindings, + hasNonClosedAlerts, + contextID, + isDraggable, + ] + ); + + return { openDetailsPanel, isLinkEnabled }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx index abf7d5cf591dd..6df18796d45e5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx @@ -7,7 +7,6 @@ import React, { useCallback, useMemo } from 'react'; import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; -import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { useHasMisconfigurations } from '@kbn/cloud-security-posture/src/hooks/use_has_misconfigurations'; import { useHasVulnerabilities } from '@kbn/cloud-security-posture/src/hooks/use_has_vulnerabilities'; @@ -18,7 +17,6 @@ import { useRefetchQueryById } from '../../../entity_analytics/api/hooks/use_ref import { RISK_INPUTS_TAB_QUERY_ID } from '../../../entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs_tab'; import type { Refetch } from '../../../common/types'; import { useCalculateEntityRiskScore } from '../../../entity_analytics/api/hooks/use_calculate_entity_risk_score'; -import { useKibana } from '../../../common/lib/kibana/kibana_react'; import { hostToCriteria } from '../../../common/components/ml/criteria/host_to_criteria'; import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score'; import { useQueryInspector } from '../../../common/components/page/manage_query'; @@ -33,10 +31,10 @@ import { HostPanelHeader } from './header'; import { AnomalyTableProvider } from '../../../common/components/ml/anomaly/anomaly_table_provider'; import type { ObservedEntityData } from '../shared/components/observed_entity/types'; import { useObservedHost } from './hooks/use_observed_host'; -import { HostDetailsPanelKey } from '../host_details_left'; import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; import { HostPreviewPanelFooter } from '../host_preview/footer'; -import { EntityEventTypes } from '../../../common/lib/telemetry'; +import { useNavigateToHostDetails } from './hooks/use_navigate_to_host_details'; + export interface HostPanelProps extends Record { contextID: string; scopeId: string; @@ -67,8 +65,6 @@ export const HostPanel = ({ isDraggable, isPreviewMode, }: HostPanelProps) => { - const { telemetry } = useKibana().services; - const { openLeftPanel } = useExpandableFlyoutApi(); const { to, from, isInitializing, setQuery, deleteQuery } = useGlobalTime(); const hostNameFilterQuery = useMemo( () => (hostName ? buildHostNamesFilter([hostName]) : undefined), @@ -119,45 +115,26 @@ export const HostPanel = ({ setQuery, }); - const openTabPanel = useCallback( - (tab?: EntityDetailsLeftPanelTab) => { - telemetry.reportEvent(EntityEventTypes.RiskInputsExpandedFlyoutOpened, { - entity: 'host', - }); - - openLeftPanel({ - id: HostDetailsPanelKey, - params: { - name: hostName, - scopeId, - isRiskScoreExist, - path: tab ? { tab } : undefined, - hasMisconfigurationFindings, - hasVulnerabilitiesFindings, - hasNonClosedAlerts, - }, - }); - }, - [ - telemetry, - openLeftPanel, - hostName, - scopeId, - isRiskScoreExist, - hasMisconfigurationFindings, - hasVulnerabilitiesFindings, - hasNonClosedAlerts, - ] - ); + const { openDetailsPanel, isLinkEnabled } = useNavigateToHostDetails({ + hostName, + scopeId, + isRiskScoreExist, + hasMisconfigurationFindings, + hasVulnerabilitiesFindings, + hasNonClosedAlerts, + isPreviewMode, + contextID, + isDraggable, + }); const openDefaultPanel = useCallback( () => - openTabPanel( - isRiskScoreExist + openDetailsPanel({ + tab: isRiskScoreExist ? EntityDetailsLeftPanelTab.RISK_INPUTS - : EntityDetailsLeftPanelTab.CSP_INSIGHTS - ), - [isRiskScoreExist, openTabPanel] + : EntityDetailsLeftPanelTab.CSP_INSIGHTS, + }), + [isRiskScoreExist, openDetailsPanel] ); const observedHost = useObservedHost(hostName, scopeId); @@ -204,7 +181,8 @@ export const HostPanel = ({ contextID={contextID} scopeId={scopeId} isDraggable={!!isDraggable} - openDetailsPanel={!isPreviewMode ? openTabPanel : undefined} + openDetailsPanel={openDetailsPanel} + isLinkEnabled={isLinkEnabled} recalculatingScore={recalculatingScore} onAssetCriticalityChange={calculateEntityRiskScore} isPreviewMode={isPreviewMode} diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx index 254985b865840..2d7fc23115eb7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx @@ -31,6 +31,11 @@ export enum CspInsightLeftPanelSubTab { ALERTS = 'alertsTabId', } +export interface EntityDetailsPath { + tab: EntityDetailsLeftPanelTab; + subTab?: CspInsightLeftPanelSubTab; +} + export interface PanelHeaderProps { /** * Id of the tab selected in the parent component to display its content diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user.test.tsx index 632f32dca57b0..b02f81b0f445e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user.test.tsx @@ -20,6 +20,7 @@ describe('ManagedUser', () => { scopeId: '', isDraggable: false, openDetailsPanel: () => {}, + isLinkEnabled: true, }; it('renders', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user.tsx index a67de667612c8..48cb42e2a4335 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user.tsx @@ -18,7 +18,7 @@ import { import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/css'; -import type { EntityDetailsLeftPanelTab } from '../../shared/components/left_panel/left_panel_header'; +import type { EntityDetailsPath } from '../../shared/components/left_panel/left_panel_header'; import { UserAssetTableType } from '../../../../explore/users/store/model'; import type { ManagedUserFields } from '../../../../../common/search_strategy/security_solution/users/managed_details'; import { ManagedUserDatasetKey } from '../../../../../common/search_strategy/security_solution/users/managed_details'; @@ -44,11 +44,15 @@ export const ManagedUser = ({ contextID, isDraggable, openDetailsPanel, + isPreviewMode, + isLinkEnabled, }: { managedUser: ManagedUserData; contextID: string; isDraggable: boolean; - openDetailsPanel?: (tab: EntityDetailsLeftPanelTab) => void; + openDetailsPanel: (path: EntityDetailsPath) => void; + isPreviewMode?: boolean; + isLinkEnabled: boolean; }) => { const entraManagedUser = managedUser.data?.[ManagedUserDatasetKey.ENTRA]; const oktaManagedUser = managedUser.data?.[ManagedUserDatasetKey.OKTA]; @@ -127,6 +131,8 @@ export const ManagedUser = ({ managedUser={entraManagedUser.fields} tableType={UserAssetTableType.assetEntra} openDetailsPanel={openDetailsPanel} + isLinkEnabled={isLinkEnabled} + isPreviewMode={isPreviewMode} > { managedUser={mockEntraUserFields} tableType={UserAssetTableType.assetEntra} openDetailsPanel={() => {}} + isLinkEnabled > @@ -28,5 +29,45 @@ describe('ManagedUserAccordion', () => { ); expect(getByTestId('test-children')).toBeInTheDocument(); + expect(getByTestId('managed-user-accordion-userAssetEntraTitleLink')).toBeInTheDocument(); + expect(getByTestId('managed-user-accordion-userAssetEntraTitleIcon')).toBeInTheDocument(); + }); + + it('renders link without icon when in preview mode', () => { + const { getByTestId, queryByTestId } = render( + + {}} + isLinkEnabled + isPreviewMode + > + + + + ); + + expect(getByTestId('managed-user-accordion-userAssetEntraTitleLink')).toBeInTheDocument(); + expect(queryByTestId('managed-user-accordion-userAssetEntraTitleIcon')).not.toBeInTheDocument(); + }); + + it('does not render link when link is not enabled', () => { + const { queryByTestId } = render( + + {}} + isLinkEnabled={false} + > + + + + ); + + expect(queryByTestId('managed-user-accordion-userAssetEntraTitleLink')).not.toBeInTheDocument(); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user_accordion.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user_accordion.tsx index 8d9007713549e..4e4745b33fc06 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user_accordion.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/components/managed_user_accordion.tsx @@ -11,6 +11,7 @@ import React from 'react'; import { css } from '@emotion/react'; import { FormattedMessage } from '@kbn/i18n-react'; import { get } from 'lodash/fp'; +import type { EntityDetailsPath } from '../../shared/components/left_panel/left_panel_header'; import { EntityDetailsLeftPanelTab } from '../../shared/components/left_panel/left_panel_header'; import { ExpandablePanel } from '../../../shared/components/expandable_panel'; import type { ManagedUserFields } from '../../../../../common/search_strategy/security_solution/users/managed_details'; @@ -23,7 +24,9 @@ interface ManagedUserAccordionProps { title: string; managedUser: ManagedUserFields; tableType: UserAssetTableType; - openDetailsPanel?: (tab: EntityDetailsLeftPanelTab) => void; + openDetailsPanel: (path: EntityDetailsPath) => void; + isLinkEnabled: boolean; + isPreviewMode?: boolean; } export const ManagedUserAccordion: React.FC = ({ @@ -32,6 +35,8 @@ export const ManagedUserAccordion: React.FC = ({ managedUser, tableType, openDetailsPanel, + isLinkEnabled, + isPreviewMode, }) => { const xsFontSize = useEuiFontSize('xxs').fontSize; const timestamp = get('@timestamp[0]', managedUser) as unknown as string | undefined; @@ -41,7 +46,7 @@ export const ManagedUserAccordion: React.FC = ({ data-test-subj={`managed-user-accordion-${tableType}`} header={{ title, - iconType: 'arrowStart', + iconType: !isPreviewMode ? 'arrowStart' : undefined, headerContent: timestamp && ( = ({ ), link: { - callback: openDetailsPanel + callback: isLinkEnabled ? () => - openDetailsPanel( - tableType === UserAssetTableType.assetOkta - ? EntityDetailsLeftPanelTab.OKTA - : EntityDetailsLeftPanelTab.ENTRA - ) + openDetailsPanel({ + tab: + tableType === UserAssetTableType.assetOkta + ? EntityDetailsLeftPanelTab.OKTA + : EntityDetailsLeftPanelTab.ENTRA, + }) : undefined, tooltip: ( {}} recalculatingScore={false} + isLinkEnabled={true} /> )) .add('integration disabled', () => ( @@ -56,6 +57,7 @@ storiesOf('Components/UserPanelContent', module) userName={'test-user-name'} onAssetCriticalityChange={() => {}} recalculatingScore={false} + isLinkEnabled={true} /> )) .add('no managed data', () => ( @@ -74,6 +76,7 @@ storiesOf('Components/UserPanelContent', module) userName={'test-user-name'} onAssetCriticalityChange={() => {}} recalculatingScore={false} + isLinkEnabled={true} /> )) .add('no observed data', () => ( @@ -112,6 +115,7 @@ storiesOf('Components/UserPanelContent', module) userName={'test-user-name'} onAssetCriticalityChange={() => {}} recalculatingScore={false} + isLinkEnabled={true} /> )) .add('loading', () => ( @@ -154,5 +158,6 @@ storiesOf('Components/UserPanelContent', module) userName={'test-user-name'} onAssetCriticalityChange={() => {}} recalculatingScore={false} + isLinkEnabled={true} /> )); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx index 08295038a1bd8..975e780582ac6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx @@ -22,7 +22,7 @@ import { FlyoutBody } from '../../shared/components/flyout_body'; import { ObservedEntity } from '../shared/components/observed_entity'; import type { ObservedEntityData } from '../shared/components/observed_entity/types'; import { useObservedUserItems } from './hooks/use_observed_user_items'; -import type { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; +import type { EntityDetailsPath } from '../shared/components/left_panel/left_panel_header'; import { EntityInsight } from '../../../cloud_security_posture/components/entity_insight'; interface UserPanelContentProps { @@ -35,8 +35,9 @@ interface UserPanelContentProps { scopeId: string; isDraggable: boolean; onAssetCriticalityChange: () => void; - openDetailsPanel?: (tab: EntityDetailsLeftPanelTab) => void; + openDetailsPanel: (path: EntityDetailsPath) => void; isPreviewMode?: boolean; + isLinkEnabled: boolean; } export const UserPanelContent = ({ @@ -51,6 +52,7 @@ export const UserPanelContent = ({ openDetailsPanel, onAssetCriticalityChange, isPreviewMode, + isLinkEnabled, }: UserPanelContentProps) => { const observedFields = useObservedUserItems(observedUser); const isManagedUserEnable = useIsExperimentalFeatureEnabled('newUserDetailsFlyoutManagedUser'); @@ -65,6 +67,7 @@ export const UserPanelContent = ({ queryId={USER_PANEL_RISK_SCORE_QUERY_ID} openDetailsPanel={openDetailsPanel} isPreviewMode={isPreviewMode} + isLinkEnabled={isLinkEnabled} /> > @@ -73,7 +76,13 @@ export const UserPanelContent = ({ entity={{ name: userName, type: 'user' }} onChange={onAssetCriticalityChange} /> - + )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_navigate_to_user_details.test.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_navigate_to_user_details.test.ts new file mode 100644 index 0000000000000..58f9860389d69 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_navigate_to_user_details.test.ts @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { useNavigateToUserDetails } from './use_navigate_to_user_details'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { + CspInsightLeftPanelSubTab, + EntityDetailsLeftPanelTab, +} from '../../shared/components/left_panel/left_panel_header'; +import { UserDetailsPanelKey } from '../../user_details_left'; +import { UserPanelKey } from '..'; +import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock'; + +jest.mock('@kbn/expandable-flyout'); +jest.mock('../../../../common/hooks/use_experimental_features'); + +const mockedTelemetry = createTelemetryServiceMock(); +jest.mock('../../../../common/lib/kibana', () => { + const original = jest.requireActual('../../../../common/lib/kibana'); + return { + ...original, + useKibana: () => ({ + ...original.useKibana(), + services: { + ...original.useKibana().services, + telemetry: mockedTelemetry, + }, + }), + }; +}); + +const mockProps = { + userName: 'testUser', + scopeId: 'testScopeId', + isRiskScoreExist: false, + hasMisconfigurationFindings: false, + hasNonClosedAlerts: false, + contextID: 'testContextID', + isPreviewMode: false, + email: ['test@test.com'], +}; + +const tab = EntityDetailsLeftPanelTab.RISK_INPUTS; +const subTab = CspInsightLeftPanelSubTab.MISCONFIGURATIONS; + +const mockOpenLeftPanel = jest.fn(); +const mockOpenFlyout = jest.fn(); + +describe('useNavigateToUserDetails', () => { + describe('when preview navigation is enabled', () => { + beforeEach(() => { + jest.clearAllMocks(); + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ + openLeftPanel: mockOpenLeftPanel, + openFlyout: mockOpenFlyout, + }); + }); + + it('returns callback that opens details panel when not in preview mode', () => { + const { result } = renderHook(() => useNavigateToUserDetails(mockProps)); + + expect(result.current.isLinkEnabled).toBe(true); + result.current.openDetailsPanel({ tab, subTab }); + + expect(result.current.isLinkEnabled).toBe(true); + result.current.openDetailsPanel({ tab, subTab }); + + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ + id: UserDetailsPanelKey, + params: { + user: { + name: mockProps.userName, + email: mockProps.email, + }, + scopeId: mockProps.scopeId, + isRiskScoreExist: mockProps.isRiskScoreExist, + path: { tab, subTab }, + hasMisconfigurationFindings: mockProps.hasMisconfigurationFindings, + hasNonClosedAlerts: mockProps.hasNonClosedAlerts, + }, + }); + }); + + it('returns callback that opens flyout when in preview mode', () => { + const { result } = renderHook(() => + useNavigateToUserDetails({ ...mockProps, isPreviewMode: true }) + ); + + expect(result.current.isLinkEnabled).toBe(true); + result.current.openDetailsPanel({ tab, subTab }); + + expect(mockOpenFlyout).toHaveBeenCalledWith({ + right: { + id: UserPanelKey, + params: { + contextID: mockProps.contextID, + scopeId: mockProps.scopeId, + userName: mockProps.userName, + }, + }, + left: { + id: UserDetailsPanelKey, + params: { + user: { + name: mockProps.userName, + email: mockProps.email, + }, + scopeId: mockProps.scopeId, + isRiskScoreExist: mockProps.isRiskScoreExist, + path: { tab, subTab }, + hasMisconfigurationFindings: mockProps.hasMisconfigurationFindings, + hasNonClosedAlerts: mockProps.hasNonClosedAlerts, + }, + }, + }); + expect(mockOpenLeftPanel).not.toHaveBeenCalled(); + }); + }); + + describe('when preview navigation is disabled', () => { + beforeEach(() => { + jest.clearAllMocks(); + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ + openLeftPanel: mockOpenLeftPanel, + openFlyout: mockOpenFlyout, + }); + }); + + it('returns callback that opens details panel when not in preview mode', () => { + const { result } = renderHook(() => useNavigateToUserDetails(mockProps)); + + expect(result.current.isLinkEnabled).toBe(true); + result.current.openDetailsPanel({ tab, subTab }); + + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ + id: UserDetailsPanelKey, + params: { + user: { + name: mockProps.userName, + email: mockProps.email, + }, + scopeId: mockProps.scopeId, + isRiskScoreExist: mockProps.isRiskScoreExist, + path: { tab, subTab }, + hasMisconfigurationFindings: mockProps.hasMisconfigurationFindings, + hasNonClosedAlerts: mockProps.hasNonClosedAlerts, + }, + }); + expect(mockOpenFlyout).not.toHaveBeenCalled(); + }); + + it('returns empty callback and isLinkEnabled is false when in preview mode', () => { + const { result } = renderHook(() => + useNavigateToUserDetails({ ...mockProps, isPreviewMode: true }) + ); + + expect(result.current.isLinkEnabled).toBe(false); + result.current.openDetailsPanel({ tab, subTab }); + + expect(mockOpenLeftPanel).not.toHaveBeenCalled(); + expect(mockOpenFlyout).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_navigate_to_user_details.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_navigate_to_user_details.ts new file mode 100644 index 0000000000000..1eed953f703b6 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_navigate_to_user_details.ts @@ -0,0 +1,114 @@ +/* + * 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 { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useCallback } from 'react'; +import type { EntityDetailsPath } from '../../shared/components/left_panel/left_panel_header'; +import { UserPanelKey } from '..'; +import { useKibana } from '../../../../common/lib/kibana'; +import { EntityEventTypes } from '../../../../common/lib/telemetry'; +import { UserDetailsPanelKey } from '../../user_details_left'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; + +interface UseNavigateToUserDetailsParams { + userName: string; + email?: string[]; + scopeId: string; + contextID: string; + isDraggable?: boolean; + isRiskScoreExist: boolean; + hasMisconfigurationFindings: boolean; + hasNonClosedAlerts: boolean; + isPreviewMode?: boolean; +} + +interface UseNavigateToUserDetailsResult { + /** + * Opens the user details panel + */ + openDetailsPanel: (path: EntityDetailsPath) => void; + /** + * Whether the link is enabled + */ + isLinkEnabled: boolean; +} + +export const useNavigateToUserDetails = ({ + userName, + email, + scopeId, + contextID, + isDraggable, + isRiskScoreExist, + hasMisconfigurationFindings, + hasNonClosedAlerts, + isPreviewMode, +}: UseNavigateToUserDetailsParams): UseNavigateToUserDetailsResult => { + const { telemetry } = useKibana().services; + const { openLeftPanel, openFlyout } = useExpandableFlyoutApi(); + const isNewNavigationEnabled = useIsExperimentalFeatureEnabled( + 'newExpandableFlyoutNavigationEnabled' + ); + + const isLinkEnabled = !isPreviewMode || (isNewNavigationEnabled && isPreviewMode); + + const openDetailsPanel = useCallback( + (path: EntityDetailsPath) => { + telemetry.reportEvent(EntityEventTypes.RiskInputsExpandedFlyoutOpened, { entity: 'user' }); + + const left = { + id: UserDetailsPanelKey, + params: { + isRiskScoreExist, + scopeId, + user: { + name: userName, + email, + }, + path, + hasMisconfigurationFindings, + hasNonClosedAlerts, + }, + }; + + const right = { + id: UserPanelKey, + params: { + contextID, + userName, + scopeId, + isDraggable, + }, + }; + + // When new navigation is enabled, nevigation in preview is enabled and open a new flyout + if (isNewNavigationEnabled && isPreviewMode) { + openFlyout({ right, left }); + } + // When not in preview mode, open left panel as usual + else if (!isPreviewMode) { + openLeftPanel(left); + } + }, + [ + telemetry, + openLeftPanel, + isRiskScoreExist, + scopeId, + userName, + email, + hasMisconfigurationFindings, + hasNonClosedAlerts, + isNewNavigationEnabled, + isPreviewMode, + openFlyout, + contextID, + isDraggable, + ] + ); + + return { openDetailsPanel, isLinkEnabled }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx index 182740a5afa57..7d11cb80369c4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx @@ -7,7 +7,6 @@ import React, { useCallback, useMemo } from 'react'; import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; -import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { useHasMisconfigurations } from '@kbn/cloud-security-posture/src/hooks/use_has_misconfigurations'; import { TableId } from '@kbn/securitysolution-data-table'; import { useNonClosedAlerts } from '../../../cloud_security_posture/hooks/use_non_closed_alerts'; @@ -15,7 +14,6 @@ import { useRefetchQueryById } from '../../../entity_analytics/api/hooks/use_ref import type { Refetch } from '../../../common/types'; import { RISK_INPUTS_TAB_QUERY_ID } from '../../../entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs_tab'; import { useCalculateEntityRiskScore } from '../../../entity_analytics/api/hooks/use_calculate_entity_risk_score'; -import { useKibana } from '../../../common/lib/kibana/kibana_react'; import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score'; import { ManagedUserDatasetKey } from '../../../../common/search_strategy/security_solution/users/managed_details'; import { useManagedUser } from '../shared/hooks/use_managed_user'; @@ -30,12 +28,11 @@ import { FlyoutLoading } from '../../shared/components/flyout_loading'; import { FlyoutNavigation } from '../../shared/components/flyout_navigation'; import { UserPanelContent } from './content'; import { UserPanelHeader } from './header'; -import { UserDetailsPanelKey } from '../user_details_left'; import { useObservedUser } from './hooks/use_observed_user'; import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; import { UserPreviewPanelFooter } from '../user_preview/footer'; import { DETECTION_RESPONSE_ALERTS_BY_STATUS_ID } from '../../../overview/components/detection_response/alerts_by_status/types'; -import { EntityEventTypes } from '../../../common/lib/telemetry'; +import { useNavigateToUserDetails } from './hooks/use_navigate_to_user_details'; export interface UserPanelProps extends Record { contextID: string; @@ -65,7 +62,6 @@ export const UserPanel = ({ isDraggable, isPreviewMode, }: UserPanelProps) => { - const { telemetry } = useKibana().services; const userNameFilterQuery = useMemo( () => (userName ? buildUserNamesFilter([userName]) : undefined), [userName] @@ -120,47 +116,26 @@ export const UserPanel = ({ setQuery, }); - const { openLeftPanel } = useExpandableFlyoutApi(); - const openPanelTab = useCallback( - (tab?: EntityDetailsLeftPanelTab) => { - telemetry.reportEvent(EntityEventTypes.RiskInputsExpandedFlyoutOpened, { - entity: 'user', - }); - - openLeftPanel({ - id: UserDetailsPanelKey, - params: { - isRiskScoreExist: !!userRiskData?.user?.risk, - scopeId, - user: { - name: userName, - email, - }, - path: tab ? { tab } : undefined, - hasMisconfigurationFindings, - hasNonClosedAlerts, - }, - }); - }, - [ - telemetry, - openLeftPanel, - userRiskData?.user?.risk, - scopeId, - userName, - email, - hasMisconfigurationFindings, - hasNonClosedAlerts, - ] - ); + const { openDetailsPanel, isLinkEnabled } = useNavigateToUserDetails({ + userName, + email, + scopeId, + contextID, + isDraggable, + isRiskScoreExist: !!userRiskData?.user?.risk, + hasMisconfigurationFindings, + hasNonClosedAlerts, + isPreviewMode, + }); + const openPanelFirstTab = useCallback( () => - openPanelTab( - isRiskScoreExist + openDetailsPanel({ + tab: isRiskScoreExist ? EntityDetailsLeftPanelTab.RISK_INPUTS - : EntityDetailsLeftPanelTab.CSP_INSIGHTS - ), - [isRiskScoreExist, openPanelTab] + : EntityDetailsLeftPanelTab.CSP_INSIGHTS, + }), + [isRiskScoreExist, openDetailsPanel] ); const hasUserDetailsData = @@ -213,8 +188,9 @@ export const UserPanel = ({ contextID={contextID} scopeId={scopeId} isDraggable={!!isDraggable} - openDetailsPanel={!isPreviewMode ? openPanelTab : undefined} + openDetailsPanel={openDetailsPanel} isPreviewMode={isPreviewMode} + isLinkEnabled={isLinkEnabled} /> {isPreviewMode && (