[9.0] [SecuritySolution] Fix host details flyout left panel tabs (#215672) (#215801)

# Backport

This will backport the following commits from `main` to `9.0`:
- [[SecuritySolution] Fix host details flyout left panel tabs
(#215672)](https://github.com/elastic/kibana/pull/215672)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Pablo
Machado","email":"pablo.nevesmachado@elastic.co"},"sourceCommit":{"committedDate":"2025-03-24T19:47:15Z","message":"[SecuritySolution]
Fix host details flyout left panel tabs (#215672)\n\n## Summary\n\nFix
Unable to switch between Risk Contributions and Insights on
host\ndetails flyout.\n\n\n**Pre Conditions**\n1. Alerts should be
available on Kibana.\n2. Entity Risk Score must be
enabled.\n\n**Steps**\n1. Navigate to a page where the flyout is
available.\n3. For any Entity, open details flyout\n4. Expand Details
flyout (left panel).\n5. Observe that the user cannot switch between
`Risk Contributions` and\n`Insights` tabs.\n\n**Expected Result**\nThe
user should be able to switch between `Risk Contributions`
and\n`Insights` tabs.\n\n**Screen
Recording**\n\n\nhttps://github.com/user-attachments/assets/3aae6291-5b5b-49a4-83c2-ac657e4e9524\n\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"6cdbeb95377cb95df567c067c03d0fa33d182b8c","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","v9.0.0","Team:
SecuritySolution","Theme: entity_analytics","Feature:Entity
Analytics","Team:Entity
Analytics","backport:version","v8.18.0","v9.1.0","v8.19.0"],"title":"[SecuritySolution]
Fix host details flyout left panel
tabs","number":215672,"url":"https://github.com/elastic/kibana/pull/215672","mergeCommit":{"message":"[SecuritySolution]
Fix host details flyout left panel tabs (#215672)\n\n## Summary\n\nFix
Unable to switch between Risk Contributions and Insights on
host\ndetails flyout.\n\n\n**Pre Conditions**\n1. Alerts should be
available on Kibana.\n2. Entity Risk Score must be
enabled.\n\n**Steps**\n1. Navigate to a page where the flyout is
available.\n3. For any Entity, open details flyout\n4. Expand Details
flyout (left panel).\n5. Observe that the user cannot switch between
`Risk Contributions` and\n`Insights` tabs.\n\n**Expected Result**\nThe
user should be able to switch between `Risk Contributions`
and\n`Insights` tabs.\n\n**Screen
Recording**\n\n\nhttps://github.com/user-attachments/assets/3aae6291-5b5b-49a4-83c2-ac657e4e9524\n\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"6cdbeb95377cb95df567c067c03d0fa33d182b8c"}},"sourceBranch":"main","suggestedTargetBranches":["9.0","8.18","8.x"],"targetPullRequestStates":[{"branch":"9.0","label":"v9.0.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.18","label":"v8.18.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/215672","number":215672,"mergeCommit":{"message":"[SecuritySolution]
Fix host details flyout left panel tabs (#215672)\n\n## Summary\n\nFix
Unable to switch between Risk Contributions and Insights on
host\ndetails flyout.\n\n\n**Pre Conditions**\n1. Alerts should be
available on Kibana.\n2. Entity Risk Score must be
enabled.\n\n**Steps**\n1. Navigate to a page where the flyout is
available.\n3. For any Entity, open details flyout\n4. Expand Details
flyout (left panel).\n5. Observe that the user cannot switch between
`Risk Contributions` and\n`Insights` tabs.\n\n**Expected Result**\nThe
user should be able to switch between `Risk Contributions`
and\n`Insights` tabs.\n\n**Screen
Recording**\n\n\nhttps://github.com/user-attachments/assets/3aae6291-5b5b-49a4-83c2-ac657e4e9524\n\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"6cdbeb95377cb95df567c067c03d0fa33d182b8c"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Pablo Machado <pablo.nevesmachado@elastic.co>
This commit is contained in:
Kibana Machine 2025-03-24 22:31:19 +01:00 committed by GitHub
parent 813cc115e9
commit 8df649a47a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 238 additions and 49 deletions

View file

@ -0,0 +1,148 @@
/*
* 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 { renderHook, act } from '@testing-library/react';
import { useSelectedTab, useTabs } from './hooks';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { TestProviders } from '@kbn/timelines-plugin/public/mock';
import type { HostDetailsPanelProps } from '.';
import type { LeftPanelTabsType } from '../shared/components/left_panel/left_panel_header';
import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header';
jest.mock('@kbn/expandable-flyout', () => ({
useExpandableFlyoutApi: jest.fn(() => ({
openLeftPanel: jest.fn(),
})),
}));
const defaultParams: HostDetailsPanelProps = {
isRiskScoreExist: true,
name: 'testHost',
scopeId: 'test',
};
const defaultTabs: LeftPanelTabsType = [
{
id: EntityDetailsLeftPanelTab.CSP_INSIGHTS,
'data-test-subj': 'cspInsightsTab',
name: <span>{'cspInsightsTab name'}</span>,
content: <span>{'cspInsightsTab content'}</span>,
},
{
id: EntityDetailsLeftPanelTab.RISK_INPUTS,
'data-test-subj': 'riskTab',
name: <span>{'riskTab name'}</span>,
content: <span>{'riskTab content'}</span>,
},
];
describe('hooks', () => {
describe('useSelectedTab', () => {
const mockOpenLeftPanel = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
(useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel });
});
it('should return the default tab when no path is provided', () => {
const { result } = renderHook(
() => useSelectedTab({ path: undefined, ...defaultParams }, defaultTabs),
{
wrapper: TestProviders,
}
);
expect(result.current.selectedTabId).toBe(EntityDetailsLeftPanelTab.CSP_INSIGHTS);
});
it('should return the tab matching the path', () => {
const { result } = renderHook(
() =>
useSelectedTab(
{ path: { tab: EntityDetailsLeftPanelTab.RISK_INPUTS }, ...defaultParams },
defaultTabs
),
{
wrapper: TestProviders,
}
);
expect(result.current.selectedTabId).toBe(EntityDetailsLeftPanelTab.RISK_INPUTS);
});
it('should call openLeftPanel with the correct parameters when setSelectedTabId is called', () => {
const { result } = renderHook(
() => useSelectedTab({ path: undefined, ...defaultParams }, defaultTabs),
{
wrapper: TestProviders,
}
);
act(() => {
result.current.setSelectedTabId(EntityDetailsLeftPanelTab.RISK_INPUTS);
});
expect(mockOpenLeftPanel).toHaveBeenCalledWith({
id: expect.any(String),
params: { ...defaultParams, path: { tab: EntityDetailsLeftPanelTab.RISK_INPUTS } },
});
});
});
describe('useTabs', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should include the risk score tab when isRiskScoreExist and name are true', () => {
const { result } = renderHook(() => useTabs(defaultParams), { wrapper: TestProviders });
expect(result.current).toEqual([
expect.objectContaining({ id: EntityDetailsLeftPanelTab.RISK_INPUTS }),
]);
});
it('should include the insights tab when any findings or alerts are present', () => {
const { result } = renderHook(
() =>
useTabs({
...defaultParams,
isRiskScoreExist: false,
hasMisconfigurationFindings: true,
hasVulnerabilitiesFindings: true,
hasNonClosedAlerts: true,
}),
{
wrapper: TestProviders,
}
);
expect(result.current).toEqual([
expect.objectContaining({ id: EntityDetailsLeftPanelTab.CSP_INSIGHTS }),
]);
});
it('should return an empty array when no tabs are available', () => {
const { result } = renderHook(
() =>
useTabs({
isRiskScoreExist: false,
name: '',
scopeId: 'scope1',
hasMisconfigurationFindings: false,
hasVulnerabilitiesFindings: false,
hasNonClosedAlerts: false,
}),
{ wrapper: TestProviders }
);
expect(result.current).toEqual([]);
});
});
});

View file

@ -0,0 +1,78 @@
/*
* 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 { useMemo } from 'react';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { EntityIdentifierFields, EntityType } from '../../../../common/entity_analytics/types';
import {
getRiskInputTab,
getInsightsInputTab,
} from '../../../entity_analytics/components/entity_details_flyout';
import type {
LeftPanelTabsType,
EntityDetailsLeftPanelTab,
} from '../shared/components/left_panel/left_panel_header';
import type { HostDetailsPanelProps } from '.';
import { HostDetailsPanelKey } from '.';
export const useSelectedTab = (params: HostDetailsPanelProps, tabs: LeftPanelTabsType) => {
const { openLeftPanel } = useExpandableFlyoutApi();
const path = params.path;
const selectedTabId = useMemo(() => {
const defaultTab = tabs.length > 0 ? tabs[0].id : undefined;
if (!path) return defaultTab;
return tabs.find((tab) => tab.id === path.tab)?.id ?? defaultTab;
}, [path, tabs]);
const setSelectedTabId = (tabId: EntityDetailsLeftPanelTab) => {
openLeftPanel({
id: HostDetailsPanelKey,
params: {
...params,
path: {
tab: tabId,
},
},
});
};
return { setSelectedTabId, selectedTabId };
};
export const useTabs = ({
isRiskScoreExist,
name,
scopeId,
hasMisconfigurationFindings,
hasVulnerabilitiesFindings,
hasNonClosedAlerts,
}: HostDetailsPanelProps): LeftPanelTabsType => {
return useMemo(() => {
const isRiskScoreTabAvailable = isRiskScoreExist && name;
const riskScoreTab = isRiskScoreTabAvailable
? [getRiskInputTab({ entityName: name, entityType: EntityType.host, scopeId })]
: [];
// Determine if the Insights tab should be included
const insightsTab =
hasMisconfigurationFindings || hasVulnerabilitiesFindings || hasNonClosedAlerts
? [getInsightsInputTab({ name, fieldName: EntityIdentifierFields.hostName })]
: [];
return [...riskScoreTab, ...insightsTab];
}, [
isRiskScoreExist,
name,
scopeId,
hasMisconfigurationFindings,
hasVulnerabilitiesFindings,
hasNonClosedAlerts,
]);
};

View file

@ -5,19 +5,15 @@
* 2.0.
*/
import React, { useEffect, useMemo, useState } from 'react';
import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
import { EntityIdentifierFields, EntityType } from '../../../../common/entity_analytics/types';
import {
getRiskInputTab,
getInsightsInputTab,
} from '../../../entity_analytics/components/entity_details_flyout';
import React from 'react';
import { type FlyoutPanelProps } from '@kbn/expandable-flyout';
import { LeftPanelContent } from '../shared/components/left_panel/left_panel_content';
import type { CspInsightLeftPanelSubTab } from '../shared/components/left_panel/left_panel_header';
import {
import type {
CspInsightLeftPanelSubTab,
EntityDetailsLeftPanelTab,
LeftPanelHeader,
} from '../shared/components/left_panel/left_panel_header';
import { LeftPanelHeader } from '../shared/components/left_panel/left_panel_header';
import { useSelectedTab, useTabs } from './hooks';
export interface HostDetailsPanelProps extends Record<string, unknown> {
isRiskScoreExist: boolean;
@ -37,46 +33,13 @@ export interface HostDetailsExpandableFlyoutProps extends FlyoutPanelProps {
}
export const HostDetailsPanelKey: HostDetailsExpandableFlyoutProps['key'] = 'host_details';
export const HostDetailsPanel = ({
name,
isRiskScoreExist,
scopeId,
path,
hasMisconfigurationFindings,
hasVulnerabilitiesFindings,
hasNonClosedAlerts,
}: HostDetailsPanelProps) => {
const [selectedTabId, setSelectedTabId] = useState(
path?.tab === EntityDetailsLeftPanelTab.CSP_INSIGHTS
? EntityDetailsLeftPanelTab.CSP_INSIGHTS
: EntityDetailsLeftPanelTab.RISK_INPUTS
);
export const HostDetailsPanel = (params: HostDetailsPanelProps) => {
const tabs = useTabs(params);
const { selectedTabId, setSelectedTabId } = useSelectedTab(params, tabs);
useEffect(() => {
if (path?.tab && path.tab !== selectedTabId) {
setSelectedTabId(path.tab);
}
}, [path?.tab, selectedTabId]);
const [tabs] = useMemo(() => {
const isRiskScoreTabAvailable = isRiskScoreExist && name;
const riskScoreTab = isRiskScoreTabAvailable
? [getRiskInputTab({ entityName: name, entityType: EntityType.host, scopeId })]
: [];
// Determine if the Insights tab should be included
const insightsTab =
hasMisconfigurationFindings || hasVulnerabilitiesFindings || hasNonClosedAlerts
? [getInsightsInputTab({ name, fieldName: EntityIdentifierFields.hostName })]
: [];
return [[...riskScoreTab, ...insightsTab], EntityDetailsLeftPanelTab.RISK_INPUTS, () => {}];
}, [
isRiskScoreExist,
name,
scopeId,
hasMisconfigurationFindings,
hasVulnerabilitiesFindings,
hasNonClosedAlerts,
]);
if (!selectedTabId) {
return null;
}
return (
<>