diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_package_policy_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_package_policy_generator.ts index 215b2ff614379b..271718d8e11fda 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_package_policy_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_package_policy_generator.ts @@ -14,7 +14,7 @@ type PartialPackagePolicy = Partial> & { inputs?: PackagePolicy['inputs']; }; -type PartialEndpointPolicyData = Partial> & { +export type PartialEndpointPolicyData = Partial> & { inputs?: PolicyData['inputs']; }; diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index e0811ef8fa821e..50ae6b40697701 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -40,6 +40,7 @@ import { import { firstNonNullValue } from './models/ecs_safety_helpers'; import type { EventOptions } from './types/generator'; import { BaseDataGenerator } from './data_generators/base_data_generator'; +import type { PartialEndpointPolicyData } from './data_generators/fleet_package_policy_generator'; import { FleetPackagePolicyGenerator } from './data_generators/fleet_package_policy_generator'; export type Event = AlertEvent | SafeEndpointEvent; @@ -1581,8 +1582,14 @@ export class EndpointDocGenerator extends BaseDataGenerator { /** * Generates a Fleet `package policy` that includes the Endpoint Policy data */ - public generatePolicyPackagePolicy(seed: string = 'seed'): PolicyData { - return new FleetPackagePolicyGenerator(seed).generateEndpointPackagePolicy(); + public generatePolicyPackagePolicy({ + seed, + overrides, + }: { + seed?: string; + overrides?: PartialEndpointPolicyData; + } = {}): PolicyData { + return new FleetPackagePolicyGenerator(seed).generateEndpointPackagePolicy(overrides); } /** diff --git a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx index 94dddba539ce20..2408dad4f39f39 100644 --- a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx +++ b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx @@ -22,12 +22,16 @@ import { EuiLoadingSpinner, EuiLink, EuiSkeletonText, + EuiCallOut, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { INTEGRATIONS_PLUGIN_ID } from '@kbn/fleet-plugin/common'; +import { pagePathGetters } from '@kbn/fleet-plugin/public'; +import type { ImmutableArray, PolicyData } from '../../../common/endpoint/types'; import { useUserPrivileges } from '../../common/components/user_privileges'; import onboardingLogo from '../images/security_administration_onboarding.svg'; -import { useKibana } from '../../common/lib/kibana'; +import { useAppUrl, useKibana } from '../../common/lib/kibana'; const TEXT_ALIGN_CENTER: CSSProperties = Object.freeze({ textAlign: 'center', @@ -103,12 +107,12 @@ const PolicyEmptyState = React.memo<{ {policyEntryPoint ? ( ) : ( )} @@ -170,107 +174,216 @@ const EndpointsEmptyState = React.memo<{ actionDisabled: boolean; handleSelectableOnChange: (o: EuiSelectableProps['options']) => void; selectionOptions: EuiSelectableProps['options']; -}>(({ loading, onActionClick, actionDisabled, handleSelectableOnChange, selectionOptions }) => { - const policySteps = useMemo( - () => [ - { - title: i18n.translate('xpack.securitySolution.endpoint.list.stepOneTitle', { - defaultMessage: 'Select the integration you want to use', - }), - children: ( + policyItems: ImmutableArray; +}>( + ({ + loading, + onActionClick, + actionDisabled, + handleSelectableOnChange, + selectionOptions, + policyItems, + }) => { + const { getAppUrl } = useAppUrl(); + const policyItemsWithoutAgentPolicy = useMemo( + () => policyItems.filter((policy) => !policy.policy_ids.length), + [policyItems] + ); + + const policiesNotAddedToAgentPolicyCallout = useMemo( + () => + !!policyItemsWithoutAgentPolicy.length && ( <> - - - - - + - {(list) => { - return loading ? ( - - - - ) : selectionOptions.length ? ( - list - ) : ( - - ); - }} - + + + + + + + +
    + {policyItemsWithoutAgentPolicy.map((policyItem) => ( +
  • + + {policyItem.name} + +
  • + ))} +
+ + + + + ), + }} + /> +
+ ), - }, - { - title: i18n.translate('xpack.securitySolution.endpoint.list.stepTwoTitle', { - defaultMessage: 'Enroll your agents enabled with Elastic Defend through Fleet', - }), - status: actionDisabled ? 'disabled' : '', - children: ( - - + [getAppUrl, policyItemsWithoutAgentPolicy] + ); + + const policySteps = useMemo( + () => [ + { + title: i18n.translate('xpack.securitySolution.endpoint.list.stepOneTitle', { + defaultMessage: 'Select the integration you want to use', + }), + children: ( + <> - - - + - - - - - ), - }, - ], - [selectionOptions, handleSelectableOnChange, loading, actionDisabled, onActionClick] - ); + {(list) => { + if (loading) { + return ( + + + + ); + } - return ( - - } - bodyComponent={ - - } - /> - ); -}); + if (!selectionOptions.length) { + return ( + + + + ); + } + + return list; + }} + + + {policiesNotAddedToAgentPolicyCallout} + + ), + }, + { + title: i18n.translate('xpack.securitySolution.endpoint.list.stepTwoTitle', { + defaultMessage: 'Enroll your agents enabled with Elastic Defend through Fleet', + }), + status: actionDisabled ? 'disabled' : '', + children: ( + + + + + + + + + + + + + ), + }, + ], + [ + selectionOptions, + loading, + handleSelectableOnChange, + policiesNotAddedToAgentPolicyCallout, + actionDisabled, + onActionClick, + ] + ); + + return ( + + } + bodyComponent={ + + } + /> + ); + } +); const ManagementEmptyState = React.memo<{ loading: boolean; @@ -284,7 +397,11 @@ const ManagementEmptyState = React.memo<{ {loading ? ( - + ) : ( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index ec27500a45e12c..56b92e4692edce 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -156,10 +156,6 @@ const getAgentAndPoliciesForEndpointsList = async ( return; } - // We use the Agent Policy API here, instead of the Package Policy, because we can't use - // filter by ID of the Saved Object. Agent Policy, however, keeps a reference (array) of - // Package Ids that it uses, thus if a reference exists there, then the package policy (policy) - // exists. const policiesFound = ( await sendBulkGetPackagePolicies(http, policyIdsToCheck) ).items.reduce( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index a5e09dc6d553c1..adfc164e98b129 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -10,6 +10,7 @@ import * as reactTestingLibrary from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { EndpointList } from '.'; import { createUseUiSetting$Mock } from '../../../../common/lib/kibana/kibana_react.mock'; +import type { DeepPartial } from '@kbn/utility-types'; import { mockEndpointDetailsApiResult, @@ -57,6 +58,8 @@ import { getEndpointPrivilegesInitialStateMock } from '../../../../common/compon import { useGetEndpointDetails } from '../../../hooks/endpoint/use_get_endpoint_details'; import { useGetAgentStatus as _useGetAgentStatus } from '../../../hooks/agents/use_get_agent_status'; import { agentStatusMocks } from '../../../../../common/endpoint/service/response_actions/mocks/agent_status.mocks'; +import { useBulkGetAgentPolicies } from '../../../services/policies/hooks'; +import type { PartialEndpointPolicyData } from '../../../../../common/endpoint/data_generators/fleet_package_policy_generator'; const mockUserPrivileges = useUserPrivileges as jest.Mock; // not sure why this can't be imported from '../../../../common/mock/formatted_relative'; @@ -85,6 +88,14 @@ jest.mock('../../../services/policies/ingest', () => { jest.mock('../../../hooks/agents/use_get_agent_status'); const useGetAgentStatusMock = _useGetAgentStatus as jest.Mock; +jest.mock('../../../services/policies/hooks', () => ({ + ...jest.requireActual('../../../services/policies/hooks'), + useBulkGetAgentPolicies: jest.fn().mockReturnValue({}), +})); +const useBulkGetAgentPoliciesMock = useBulkGetAgentPolicies as unknown as jest.Mock< + DeepPartial> +>; + const mockUseUiSetting$ = useUiSetting$ as jest.Mock; const timepickerRanges = [ { @@ -149,6 +160,7 @@ describe('when on the endpoint list page', () => { const { act, screen, fireEvent } = reactTestingLibrary; let render: () => ReturnType; + let renderResult: reactTestingLibrary.RenderResult; let history: AppContextTestRender['history']; let store: AppContextTestRender['store']; let coreStart: AppContextTestRender['coreStart']; @@ -170,7 +182,7 @@ describe('when on the endpoint list page', () => { beforeEach(() => { const mockedContext = createAppRootMockRenderer(); ({ history, store, coreStart, middlewareSpy } = mockedContext); - render = () => mockedContext.render(); + render = () => (renderResult = mockedContext.render()); reactTestingLibrary.act(() => { history.push(`${MANAGEMENT_PATH}/endpoints`); }); @@ -186,9 +198,9 @@ describe('when on the endpoint list page', () => { endpointsResults: [], }); - const renderResult = render(); + render(); const timelineFlyout = renderResult.queryByTestId('timeline-bottom-bar-title-button'); - expect(timelineFlyout).toBeNull(); + expect(timelineFlyout).not.toBeInTheDocument(); }); describe('when there are no endpoints or polices', () => { @@ -199,47 +211,200 @@ describe('when on the endpoint list page', () => { }); it('should show the empty state when there are no hosts or polices', async () => { - const renderResult = render(); + render(); await reactTestingLibrary.act(async () => { await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); }); // Initially, there are no hosts or policies, so we prompt to add policies first. const table = await renderResult.findByTestId('emptyPolicyTable'); - expect(table).not.toBeNull(); + expect(table).toBeInTheDocument(); }); }); describe('when there are policies, but no hosts', () => { - let renderResult: ReturnType; - beforeEach(async () => { - const policyData = mockPolicyResultList({ total: 3 }).items; + const getOptionsTexts = async () => { + const onboardingPolicySelect = await renderResult.findByTestId('onboardingPolicySelect'); + const options = onboardingPolicySelect.querySelectorAll('[role=option]'); + + return [...options].map(({ textContent }) => textContent); + }; + + const setupPolicyDataMocks = ( + partialPolicyData: PartialEndpointPolicyData[] = [ + { name: 'Package 1', policy_ids: ['policy-1'] }, + ] + ) => { + const policyData = partialPolicyData.map((overrides) => + docGenerator.generatePolicyPackagePolicy({ overrides }) + ); + setEndpointListApiMockImplementation(coreStart.http, { endpointsResults: [], endpointPackagePolicies: policyData, }); + }; - renderResult = render(); - await reactTestingLibrary.act(async () => { - await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); + beforeEach(async () => { + useBulkGetAgentPoliciesMock.mockReturnValue({ + data: [ + { id: 'policy-1', name: 'Agent Policy 1' }, + { id: 'policy-2', name: 'Agent Policy 2' }, + { id: 'policy-3', name: 'Agent Policy 3' }, + ], + isLoading: false, }); + + setupPolicyDataMocks(); }); + afterEach(() => { jest.clearAllMocks(); }); - it('should show the no hosts empty state', async () => { + it('should show loading spinner while Agent Policies are loading', async () => { + useBulkGetAgentPoliciesMock.mockReturnValue({ isLoading: true }); + render(); + expect( + await renderResult.findByTestId('management-empty-state-loading-spinner') + ).toBeInTheDocument(); + }); + + it('should show the no hosts empty state without loading spinner', async () => { + render(); + + expect( + renderResult.queryByTestId('management-empty-state-loading-spinner') + ).not.toBeInTheDocument(); + const emptyHostsTable = await renderResult.findByTestId('emptyHostsTable'); - expect(emptyHostsTable).not.toBeNull(); + expect(emptyHostsTable).toBeInTheDocument(); }); it('should display the onboarding steps', async () => { + render(); const onboardingSteps = await renderResult.findByTestId('onboardingSteps'); - expect(onboardingSteps).not.toBeNull(); + expect(onboardingSteps).toBeInTheDocument(); }); - it('should show policy selection', async () => { - const onboardingPolicySelect = await renderResult.findByTestId('onboardingPolicySelect'); - expect(onboardingPolicySelect).not.toBeNull(); + describe('policy selection', () => { + it('should show policy selection', async () => { + render(); + const onboardingPolicySelect = await renderResult.findByTestId('onboardingPolicySelect'); + expect(onboardingPolicySelect).toBeInTheDocument(); + }); + + it('should show discrete `package policy - agent policy` pairs', async () => { + setupPolicyDataMocks([ + { name: 'Package 1', policy_ids: ['policy-1'] }, + { name: 'Package 2', policy_ids: ['policy-2'] }, + ]); + + render(); + const optionsTexts = await getOptionsTexts(); + + expect(optionsTexts).toStrictEqual([ + 'Package 1 - Agent Policy 1', + 'Package 2 - Agent Policy 2', + ]); + }); + + it('should display the same package policy with multiple Agent Policies multiple times', async () => { + setupPolicyDataMocks([ + { name: 'Package 1', policy_ids: ['policy-1', 'policy-2', 'policy-3'] }, + ]); + + render(); + const optionsTexts = await getOptionsTexts(); + + expect(optionsTexts).toStrictEqual([ + 'Package 1 - Agent Policy 1', + 'Package 1 - Agent Policy 2', + 'Package 1 - Agent Policy 3', + ]); + }); + + it('should not display a package policy without agent policy', async () => { + setupPolicyDataMocks([ + { name: 'Package 1', policy_ids: [] }, + { name: 'Package 2', policy_ids: ['policy-1'] }, + ]); + + render(); + const optionsTexts = await getOptionsTexts(); + + expect(optionsTexts).toStrictEqual(['Package 2 - Agent Policy 1']); + }); + + it("should fallback to agent policy ID if it's not found", async () => { + setupPolicyDataMocks([{ name: 'Package 1', policy_ids: ['agent-policy-id'] }]); + + render(); + const optionsTexts = await getOptionsTexts(); + expect( + renderResult.queryByTestId('noIntegrationsAddedToAgentPoliciesCallout') + ).not.toBeInTheDocument(); + + expect(optionsTexts).toStrictEqual(['Package 1 - agent-policy-id']); + }); + + it('should show callout indicating that none of the integrations are added to agent policies', async () => { + setupPolicyDataMocks([{ name: 'Package 1', policy_ids: [] }]); + + render(); + + expect( + await renderResult.findByTestId('noIntegrationsAddedToAgentPoliciesCallout') + ).toBeInTheDocument(); + }); + }); + + describe('integration not added to agent policy callout', () => { + it('should not display callout if all integrations are added to agent policies', async () => { + setupPolicyDataMocks([ + { name: 'Package 1', policy_ids: ['policy-1'] }, + { name: 'Package 2', policy_ids: ['policy-2'] }, + ]); + + render(); + await getOptionsTexts(); + + expect( + renderResult.queryByTestId('integrationsNotAddedToAgentPolicyCallout') + ).not.toBeInTheDocument(); + }); + + it('should display callout if an integration is not added to an agent policy', async () => { + setupPolicyDataMocks([ + { name: 'Package 1', policy_ids: ['policy-1'] }, + { name: 'Package 2', policy_ids: [] }, + ]); + + render(); + + expect( + await renderResult.findByTestId('integrationsNotAddedToAgentPolicyCallout') + ).toBeInTheDocument(); + }); + + it('should list all integrations which are not added to an agent policy', async () => { + setupPolicyDataMocks([ + { name: 'Package 1', policy_ids: ['policy-1'] }, + { name: 'Package 2', policy_ids: [] }, + { name: 'Package 3', policy_ids: [] }, + { name: 'Package 4', policy_ids: [] }, + ]); + + render(); + + const integrations = await renderResult.findAllByTestId( + 'integrationWithoutAgentPolicyListItem' + ); + expect(integrations.map(({ textContent }) => textContent)).toStrictEqual([ + 'Package 2', + 'Package 3', + 'Package 4', + ]); + }); }); }); @@ -349,7 +514,7 @@ describe('when on the endpoint list page', () => { }); it('should display rows in the table', async () => { - const renderResult = render(); + render(); await reactTestingLibrary.act(async () => { await middlewareSpy.waitForAction('serverReturnedEndpointList'); }); @@ -357,7 +522,7 @@ describe('when on the endpoint list page', () => { expect(rows).toHaveLength(6); }); it('should show total', async () => { - const renderResult = render(); + render(); await reactTestingLibrary.act(async () => { await middlewareSpy.waitForAction('serverReturnedEndpointList'); }); @@ -365,7 +530,7 @@ describe('when on the endpoint list page', () => { expect(total.textContent).toEqual('Showing 5 endpoints'); }); it('should agent status', async () => { - const renderResult = render(); + render(); await reactTestingLibrary.act(async () => { await middlewareSpy.waitForAction('serverReturnedEndpointList'); }); @@ -380,7 +545,7 @@ describe('when on the endpoint list page', () => { }); it('should display correct policy status', async () => { - const renderResult = render(); + render(); await reactTestingLibrary.act(async () => { await middlewareSpy.waitForAction('serverReturnedEndpointList'); }); @@ -394,12 +559,12 @@ describe('when on the endpoint list page', () => { POLICY_STATUS_TO_HEALTH_COLOR[generatedPolicyStatuses[index]] }]` ) - ).not.toBeNull(); + ).toBeInTheDocument(); }); }); it('should display policy out-of-date warning when changes pending', async () => { - const renderResult = render(); + render(); await reactTestingLibrary.act(async () => { await middlewareSpy.waitForAction('serverReturnedEndpointList'); }); @@ -412,12 +577,12 @@ describe('when on the endpoint list page', () => { }); it('should display policy name as a link', async () => { - const renderResult = render(); + render(); await reactTestingLibrary.act(async () => { await middlewareSpy.waitForAction('serverReturnedEndpointList'); }); const firstPolicyName = (await renderResult.findAllByTestId('policyNameCellLink-link'))[0]; - expect(firstPolicyName).not.toBeNull(); + expect(firstPolicyName).toBeInTheDocument(); expect(firstPolicyName.getAttribute('href')).toEqual( `${APP_PATH}${MANAGEMENT_PATH}/policy/${firstPolicyID}/settings` ); @@ -425,7 +590,6 @@ describe('when on the endpoint list page', () => { describe('when the user clicks the first hostname in the table', () => { const endpointDetails: HostInfo = mockEndpointDetailsApiResult(); - let renderResult: reactTestingLibrary.RenderResult; beforeEach(async () => { mockUseGetEndpointDetails.mockReturnValue({ data: { @@ -447,7 +611,7 @@ describe('when on the endpoint list page', () => { }, }, }); - renderResult = render(); + render(); await reactTestingLibrary.act(async () => { await middlewareSpy.waitForAction('serverReturnedEndpointList'); }); @@ -459,20 +623,20 @@ describe('when on the endpoint list page', () => { it('should show the flyout', async () => { return renderResult.findByTestId('endpointDetailsFlyout').then((flyout) => { - expect(flyout).not.toBeNull(); + expect(flyout).toBeInTheDocument(); }); }); }); it('should show revision number', async () => { - const renderResult = render(); + render(); await reactTestingLibrary.act(async () => { await middlewareSpy.waitForAction('serverReturnedEndpointList'); }); const firstPolicyRevElement = ( await renderResult.findAllByTestId('policyNameCellLink-revision') )[0]; - expect(firstPolicyRevElement).not.toBeNull(); + expect(firstPolicyRevElement).toBeInTheDocument(); expect(firstPolicyRevElement.textContent).toEqual(`rev. ${firstPolicyRev}`); }); }); @@ -502,7 +666,7 @@ describe('when on the endpoint list page', () => { }); it('should update data after some time', async () => { - let renderResult = render(); + render(); await reactTestingLibrary.act(async () => { await middlewareSpy.waitForAction('serverReturnedEndpointList'); }); @@ -518,7 +682,7 @@ describe('when on the endpoint list page', () => { await middlewareSpy.waitForAction('serverReturnedEndpointList'); }); - renderResult = render(); + render(); const updatedTotal = await renderResult.findAllByTestId('endpointListTableTotal'); expect(updatedTotal[0].textContent).toEqual('1 Host'); @@ -601,33 +765,33 @@ describe('when on the endpoint list page', () => { }); it('should show the flyout and footer', async () => { - const renderResult = render(); - expect(renderResult.getByTestId('endpointDetailsFlyout')).not.toBeNull(); - expect(renderResult.getByTestId('endpointDetailsFlyoutFooter')).not.toBeNull(); + render(); + expect(renderResult.getByTestId('endpointDetailsFlyout')).toBeInTheDocument(); + expect(renderResult.getByTestId('endpointDetailsFlyoutFooter')).toBeInTheDocument(); }); it('should display policy name value as a link', async () => { - const renderResult = render(); + render(); const policyDetailsLink = await renderResult.findByTestId('policyNameCellLink-link'); - expect(policyDetailsLink).not.toBeNull(); + expect(policyDetailsLink).toBeInTheDocument(); expect(policyDetailsLink.getAttribute('href')).toEqual( `${APP_PATH}${MANAGEMENT_PATH}/policy/${hostInfo.metadata.Endpoint.policy.applied.id}/settings` ); }); it('should display policy revision number', async () => { - const renderResult = render(); + render(); const policyDetailsRevElement = await renderResult.findByTestId( 'policyNameCellLink-revision' ); - expect(policyDetailsRevElement).not.toBeNull(); + expect(policyDetailsRevElement).toBeInTheDocument(); expect(policyDetailsRevElement.textContent).toEqual( `rev. ${hostInfo.metadata.Endpoint.policy.applied.endpoint_policy_version}` ); }); it('should update the URL when policy name link is clicked', async () => { - const renderResult = render(); + render(); const policyDetailsLink = await renderResult.findByTestId('policyNameCellLink-link'); const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { @@ -640,7 +804,7 @@ describe('when on the endpoint list page', () => { }); it('should update the URL when policy status link is clicked', async () => { - const renderResult = render(); + render(); const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { @@ -654,7 +818,7 @@ describe('when on the endpoint list page', () => { it('should display Success overall policy status', async () => { getMockUseEndpointDetails(HostPolicyResponseActionStatus.success); - const renderResult = render(); + render(); const policyStatusBadge = await renderResult.findByTestId('policyStatusValue'); expect(renderResult.getByTestId('policyStatusValue-success')).toBeTruthy(); expect(policyStatusBadge.textContent).toEqual('Success'); @@ -662,7 +826,7 @@ describe('when on the endpoint list page', () => { it('should display Warning overall policy status', async () => { getMockUseEndpointDetails(HostPolicyResponseActionStatus.warning); - const renderResult = render(); + render(); const policyStatusBadge = await renderResult.findByTestId('policyStatusValue'); expect(policyStatusBadge.textContent).toEqual('Warning'); expect(renderResult.getByTestId('policyStatusValue-warning')).toBeTruthy(); @@ -670,7 +834,7 @@ describe('when on the endpoint list page', () => { it('should display Failed overall policy status', async () => { getMockUseEndpointDetails(HostPolicyResponseActionStatus.failure); - const renderResult = render(); + render(); const policyStatusBadge = await renderResult.findByTestId('policyStatusValue'); expect(policyStatusBadge.textContent).toEqual('Failed'); expect(renderResult.getByTestId('policyStatusValue-failure')).toBeTruthy(); @@ -678,15 +842,15 @@ describe('when on the endpoint list page', () => { it('should display Unknown overall policy status', async () => { getMockUseEndpointDetails('' as HostPolicyResponseActionStatus); - const renderResult = render(); + render(); const policyStatusBadge = await renderResult.findByTestId('policyStatusValue'); expect(policyStatusBadge.textContent).toEqual('Unknown'); expect(renderResult.getByTestId('policyStatusValue-')).toBeTruthy(); }); it('should show the Take Action button', async () => { - const renderResult = render(); - expect(renderResult.getByTestId('endpointDetailsActionsButton')).not.toBeNull(); + render(); + expect(renderResult.getByTestId('endpointDetailsActionsButton')).toBeInTheDocument(); }); describe('Activity Log tab', () => { @@ -705,8 +869,8 @@ describe('when on the endpoint list page', () => { }); describe('when `canReadActionsLogManagement` is TRUE', () => { - it('should start with the activity log tab as unselected', async () => { - const renderResult = await render(); + it('should start with the activity log tab as unselected', () => { + render(); const detailsTab = renderResult.getByTestId('endpoint-details-flyout-tab-details'); const activityLogTab = renderResult.getByTestId( 'endpoint-details-flyout-tab-activity_log' @@ -714,12 +878,14 @@ describe('when on the endpoint list page', () => { expect(detailsTab).toHaveAttribute('aria-selected', 'true'); expect(activityLogTab).toHaveAttribute('aria-selected', 'false'); - expect(renderResult.getByTestId('endpointDetailsFlyoutBody')).not.toBeNull(); - expect(renderResult.queryByTestId('endpointActivityLogFlyoutBody')).toBeNull(); + expect(renderResult.getByTestId('endpointDetailsFlyoutBody')).toBeInTheDocument(); + expect( + renderResult.queryByTestId('endpointActivityLogFlyoutBody') + ).not.toBeInTheDocument(); }); it('should show the activity log content when selected', async () => { - const renderResult = await render(); + render(); const detailsTab = renderResult.getByTestId('endpoint-details-flyout-tab-details'); const activityLogTab = renderResult.getByTestId( 'endpoint-details-flyout-tab-activity_log' @@ -728,13 +894,13 @@ describe('when on the endpoint list page', () => { await userEvent.click(activityLogTab); expect(detailsTab).toHaveAttribute('aria-selected', 'false'); expect(activityLogTab).toHaveAttribute('aria-selected', 'true'); - expect(renderResult.getByTestId('endpointActivityLogFlyoutBody')).not.toBeNull(); - expect(renderResult.queryByTestId('endpointDetailsFlyoutBody')).toBeNull(); + expect(renderResult.getByTestId('endpointActivityLogFlyoutBody')).toBeInTheDocument(); + expect(renderResult.queryByTestId('endpointDetailsFlyoutBody')).not.toBeInTheDocument(); }); }); describe('when `canReadActionsLogManagement` is FALSE', () => { - it('should not show the response actions history tab', async () => { + it('should not show the response actions history tab', () => { mockUserPrivileges.mockReturnValue({ ...mockInitialUserPrivilegesState(), endpointPrivileges: { @@ -744,15 +910,15 @@ describe('when on the endpoint list page', () => { canAccessFleet: true, }, }); - const renderResult = await render(); + render(); const detailsTab = renderResult.getByTestId('endpoint-details-flyout-tab-details'); const activityLogTab = renderResult.queryByTestId( 'endpoint-details-flyout-tab-activity_log' ); expect(detailsTab).toHaveAttribute('aria-selected', 'true'); - expect(activityLogTab).toBeNull(); - expect(renderResult.findByTestId('endpointDetailsFlyoutBody')).not.toBeNull(); + expect(activityLogTab).not.toBeInTheDocument(); + expect(renderResult.getByTestId('endpointDetailsFlyoutBody')).toBeInTheDocument(); }); it('should show the overview tab when force loading actions history tab via URL', async () => { @@ -769,7 +935,7 @@ describe('when on the endpoint list page', () => { history.push(`${MANAGEMENT_PATH}/endpoints?selected_endpoint=1&show=activity_log`); }); - const renderResult = await render(); + render(); await middlewareSpy.waitForAction('serverFinishedInitialization'); const detailsTab = renderResult.getByTestId('endpoint-details-flyout-tab-details'); @@ -778,14 +944,13 @@ describe('when on the endpoint list page', () => { ); expect(detailsTab).toHaveAttribute('aria-selected', 'true'); - expect(activityLogTab).toBeNull(); - expect(renderResult.findByTestId('endpointDetailsFlyoutBody')).not.toBeNull(); + expect(activityLogTab).not.toBeInTheDocument(); + expect(renderResult.getByTestId('endpointDetailsFlyoutBody')).toBeInTheDocument(); }); }); }); describe('when showing host Policy Response panel', () => { - let renderResult: ReturnType; beforeEach(async () => { coreStart.http.post.mockImplementation(async (requestOptions) => { if (requestOptions.path === HOST_METADATA_LIST_ROUTE) { @@ -793,7 +958,7 @@ describe('when on the endpoint list page', () => { } throw new Error(`POST to '${requestOptions.path}' does not have a mock response!`); }); - renderResult = await render(); + render(); const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { @@ -806,14 +971,14 @@ describe('when on the endpoint list page', () => { it('should hide the host details panel', async () => { const endpointDetailsFlyout = renderResult.queryByTestId('endpointDetailsFlyoutBody'); - expect(endpointDetailsFlyout).toBeNull(); + expect(endpointDetailsFlyout).not.toBeInTheDocument(); }); it('should display policy response sub-panel', async () => { - expect(await renderResult.findByTestId('flyoutSubHeaderBackButton')).not.toBeNull(); + expect(await renderResult.findByTestId('flyoutSubHeaderBackButton')).toBeInTheDocument(); expect( await renderResult.findByTestId('endpointDetailsPolicyResponseFlyoutBody') - ).not.toBeNull(); + ).toBeInTheDocument(); }); it('should include the back to details link', async () => { @@ -862,14 +1027,13 @@ describe('when on the endpoint list page', () => { }; let isolateApiMock: ReturnType; - let renderResult: ReturnType; beforeEach(async () => { getKibanaServicesMock.mockReturnValue(coreStart); reactTestingLibrary.act(() => { history.push(`${MANAGEMENT_PATH}/endpoints?selected_endpoint=1&show=isolate`); }); - renderResult = render(); + render(); await middlewareSpy.waitForAction('serverFinishedInitialization'); // Need to reset `http.post` and adjust it so that the mock for http host @@ -880,7 +1044,7 @@ describe('when on the endpoint list page', () => { }); it('should show the isolate form', () => { - expect(renderResult.getByTestId('host_isolation_comment')).not.toBeNull(); + expect(renderResult.getByTestId('host_isolation_comment')).toBeInTheDocument(); }); it('should take you back to details when back link below the flyout header is clicked', async () => { @@ -922,7 +1086,7 @@ describe('when on the endpoint list page', () => { it('should isolate endpoint host when confirm is clicked', async () => { await confirmIsolateAndWaitForApiResponse(); - expect(renderResult.getByTestId('hostIsolateSuccessMessage')).not.toBeNull(); + expect(renderResult.getByTestId('hostIsolateSuccessMessage')).toBeInTheDocument(); }); it('should navigate to details when the Complete button on success message is clicked', async () => { @@ -946,7 +1110,7 @@ describe('when on the endpoint list page', () => { }); await confirmIsolateAndWaitForApiResponse('failure'); - expect(renderResult.getByText('oh oh. something went wrong')).not.toBeNull(); + expect(renderResult.getByText('oh oh. something went wrong')).toBeInTheDocument(); }); it('should reset isolation state and show form again', async () => { @@ -954,7 +1118,7 @@ describe('when on the endpoint list page', () => { // (`show` is NOT `isolate`), then the state should be reset so that the form show up again the next // time `isolate host` is clicked await confirmIsolateAndWaitForApiResponse(); - expect(renderResult.getByTestId('hostIsolateSuccessMessage')).not.toBeNull(); + expect(renderResult.getByTestId('hostIsolateSuccessMessage')).toBeInTheDocument(); // Close flyout const changeUrlAction = middlewareSpy.waitForAction('userChangedUrl'); @@ -975,7 +1139,7 @@ describe('when on the endpoint list page', () => { }); it('should NOT show the flyout footer', () => { - expect(renderResult.queryByTestId('endpointDetailsFlyoutFooter')).toBeNull(); + expect(renderResult.queryByTestId('endpointDetailsFlyoutFooter')).not.toBeInTheDocument(); }); }); }); @@ -985,7 +1149,6 @@ describe('when on the endpoint list page', () => { let hostInfo: HostInfo[]; let agentId: string; let agentPolicyId: string; - let renderResult: ReturnType; let endpointActionsButton: HTMLElement; // 2nd endpoint only has isolation capabilities @@ -1069,7 +1232,7 @@ describe('when on the endpoint list page', () => { history.push(`${MANAGEMENT_PATH}/endpoints`); }); - renderResult = render(); + render(); await middlewareSpy.waitForAction('serverReturnedEndpointList'); await middlewareSpy.waitForAction('serverReturnedEndpointAgentPolicies'); @@ -1130,7 +1293,7 @@ describe('when on the endpoint list page', () => { reactTestingLibrary.fireEvent.click(endpointActionsButton); }); const isolateLink = screen.queryByTestId('isolateLink'); - expect(isolateLink).toBeNull(); + expect(isolateLink).not.toBeInTheDocument(); }); it('navigates to the Security Solution Host Details page', async () => { @@ -1179,7 +1342,7 @@ describe('when on the endpoint list page', () => { }); render(); const banner = screen.queryByTestId('callout-endpoints-list-transform-failed'); - expect(banner).toBeNull(); + expect(banner).not.toBeInTheDocument(); }); it('is not displayed when non-relevant transform is failing', () => { @@ -1193,7 +1356,7 @@ describe('when on the endpoint list page', () => { }); render(); const banner = screen.queryByTestId('callout-endpoints-list-transform-failed'); - expect(banner).toBeNull(); + expect(banner).not.toBeInTheDocument(); }); it('is not displayed when no endpoint policy', () => { @@ -1207,7 +1370,7 @@ describe('when on the endpoint list page', () => { }); render(); const banner = screen.queryByTestId('callout-endpoints-list-transform-failed'); - expect(banner).toBeNull(); + expect(banner).not.toBeInTheDocument(); }); it('is displayed when relevant transform state is failed state', async () => { @@ -1268,12 +1431,12 @@ describe('when on the endpoint list page', () => { canAccessFleet: true, }), }); - const renderResult = render(); + render(); await reactTestingLibrary.act(async () => { await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); }); const onboardingSteps = await renderResult.findByTestId('onboardingSteps'); - expect(onboardingSteps).not.toBeNull(); + expect(onboardingSteps).toBeInTheDocument(); }); it('user has endpoint list READ and fleet All and can view entire onboarding screen', async () => { mockUserPrivileges.mockReturnValue({ @@ -1283,12 +1446,12 @@ describe('when on the endpoint list page', () => { canAccessFleet: true, }), }); - const renderResult = render(); + render(); await reactTestingLibrary.act(async () => { await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); }); const onboardingSteps = await renderResult.findByTestId('onboardingSteps'); - expect(onboardingSteps).not.toBeNull(); + expect(onboardingSteps).toBeInTheDocument(); }); it('user has endpoint list ALL/READ and fleet NONE and can view a modified onboarding screen with no actions link to fleet', async () => { mockUserPrivileges.mockReturnValue({ @@ -1298,28 +1461,26 @@ describe('when on the endpoint list page', () => { canAccessFleet: false, }), }); - const renderResult = render(); + render(); await reactTestingLibrary.act(async () => { await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); }); const onboardingSteps = await renderResult.findByTestId('policyOnboardingInstructions'); - expect(onboardingSteps).not.toBeNull(); + expect(onboardingSteps).toBeInTheDocument(); const noPrivilegesPage = await renderResult.findByTestId('noFleetAccess'); - expect(noPrivilegesPage).not.toBeNull(); + expect(noPrivilegesPage).toBeInTheDocument(); const startButton = renderResult.queryByTestId('onboardingStartButton'); - expect(startButton).toBeNull(); + expect(startButton).not.toBeInTheDocument(); }); }); describe('endpoint list take action with RBAC controls', () => { - let renderResult: ReturnType; - const renderAndClickActionsButton = async (tableRow: number = 0) => { reactTestingLibrary.act(() => { history.push(`${MANAGEMENT_PATH}/endpoints`); }); - renderResult = render(); + render(); await middlewareSpy.waitForAction('serverReturnedEndpointList'); await middlewareSpy.waitForAction('serverReturnedEndpointAgentPolicies'); @@ -1408,7 +1569,7 @@ describe('when on the endpoint list page', () => { }); await renderAndClickActionsButton(); const isolateLink = await renderResult.findByTestId('isolateLink'); - expect(isolateLink).not.toBeNull(); + expect(isolateLink).toBeInTheDocument(); }); it('hides Isolate host option if canIsolateHost is NONE', async () => { mockUserPrivileges.mockReturnValue({ @@ -1420,7 +1581,7 @@ describe('when on the endpoint list page', () => { }); await renderAndClickActionsButton(); const isolateLink = screen.queryByTestId('isolateLink'); - expect(isolateLink).toBeNull(); + expect(isolateLink).not.toBeInTheDocument(); }); it('shows unisolate host option if canUnHostIsolate is READ/ALL', async () => { mockUserPrivileges.mockReturnValue({ @@ -1432,7 +1593,7 @@ describe('when on the endpoint list page', () => { }); await renderAndClickActionsButton(1); const unisolateLink = await renderResult.findByTestId('unIsolateLink'); - expect(unisolateLink).not.toBeNull(); + expect(unisolateLink).toBeInTheDocument(); }); it('hides unisolate host option if canUnIsolateHost is NONE', async () => { mockUserPrivileges.mockReturnValue({ @@ -1444,7 +1605,7 @@ describe('when on the endpoint list page', () => { }); await renderAndClickActionsButton(1); const unisolateLink = renderResult.queryByTestId('unIsolateLink'); - expect(unisolateLink).toBeNull(); + expect(unisolateLink).not.toBeInTheDocument(); }); it('shows the Responder option when at least one rbac privilege from host isolation, process operation and file operation, is set to TRUE', async () => { @@ -1457,7 +1618,7 @@ describe('when on the endpoint list page', () => { }); await renderAndClickActionsButton(); const responderButton = await renderResult.findByTestId('console'); - expect(responderButton).not.toBeNull(); + expect(responderButton).toBeInTheDocument(); }); it('hides the Responder option when host isolation, process operation and file operations are ALL set to NONE', async () => { @@ -1470,13 +1631,13 @@ describe('when on the endpoint list page', () => { }); await renderAndClickActionsButton(); const responderButton = renderResult.queryByTestId('console'); - expect(responderButton).toBeNull(); + expect(responderButton).not.toBeInTheDocument(); }); it('always shows the Host details link', async () => { mockUserPrivileges.mockReturnValue(getUserPrivilegesMockDefaultValue()); await renderAndClickActionsButton(); const hostLink = await renderResult.findByTestId('hostLink'); - expect(hostLink).not.toBeNull(); + expect(hostLink).toBeInTheDocument(); }); it('shows Agent Policy, View Agent Details and Reassign Policy Links when canReadFleetAgents,canWriteFleetAgents,canReadFleetAgentPolicies RBAC control is enabled', async () => { mockUserPrivileges.mockReturnValue({ @@ -1493,9 +1654,9 @@ describe('when on the endpoint list page', () => { const agentPolicyLink = await renderResult.findByTestId('agentPolicyLink'); const agentDetailsLink = await renderResult.findByTestId('agentDetailsLink'); const agentPolicyReassignLink = await renderResult.findByTestId('agentPolicyReassignLink'); - expect(agentPolicyLink).not.toBeNull(); - expect(agentDetailsLink).not.toBeNull(); - expect(agentPolicyReassignLink).not.toBeNull(); + expect(agentPolicyLink).toBeInTheDocument(); + expect(agentDetailsLink).toBeInTheDocument(); + expect(agentPolicyReassignLink).toBeInTheDocument(); }); it('hides Agent Policy, View Agent Details and Reassign Policy Links when canAccessFleet RBAC control is NOT enabled', async () => { mockUserPrivileges.mockReturnValue({ @@ -1509,9 +1670,9 @@ describe('when on the endpoint list page', () => { const agentPolicyLink = renderResult.queryByTestId('agentPolicyLink'); const agentDetailsLink = renderResult.queryByTestId('agentDetailsLink'); const agentPolicyReassignLink = renderResult.queryByTestId('agentPolicyReassignLink'); - expect(agentPolicyLink).toBeNull(); - expect(agentDetailsLink).toBeNull(); - expect(agentPolicyReassignLink).toBeNull(); + expect(agentPolicyLink).not.toBeInTheDocument(); + expect(agentDetailsLink).not.toBeInTheDocument(); + expect(agentPolicyReassignLink).not.toBeInTheDocument(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 2ed2b20ab78a7b..162d05f54ec21b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -40,7 +40,7 @@ import { EndpointListNavLink } from './components/endpoint_list_nav_link'; import { AgentStatus } from '../../../../common/components/endpoint/agents/agent_status'; import { EndpointDetailsFlyout } from './details'; import * as selectors from '../store/selectors'; -import { nonExistingPolicies } from '../store/selectors'; +import type { nonExistingPolicies } from '../store/selectors'; import { useEndpointSelector } from './hooks'; import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants'; import type { CreateStructuredSelector } from '../../../../common/store'; @@ -69,6 +69,7 @@ import { APP_UI_ID } from '../../../../../common/constants'; import { ManagementEmptyStateWrapper } from '../../../components/management_empty_state_wrapper'; import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { BackToPolicyListButton } from './components/back_to_policy_list_button'; +import { useBulkGetAgentPolicies } from '../../../services/policies/hooks'; const MAX_PAGINATED_ITEM = 9999; @@ -338,8 +339,8 @@ export const EndpointList = () => { patternsError, metadataTransformStats, isInitialized, + nonExistingPolicies: missingPolicies, } = useEndpointSelector(selector); - const missingPolicies = useEndpointSelector(nonExistingPolicies); const { canReadEndpointList, canAccessFleet, @@ -353,24 +354,22 @@ export const EndpointList = () => { // cap ability to page at 10k records. (max_result_window) const maxPageCount = totalItemCount > MAX_PAGINATED_ITEM ? MAX_PAGINATED_ITEM : totalItemCount; - const hasPolicyData = useMemo(() => policyItems && policyItems.length > 0, [policyItems]); - const hasListData = useMemo(() => listData && listData.length > 0, [listData]); + const hasPolicyData = policyItems && policyItems.length > 0; + const hasListData = listData && listData.length > 0; const refreshStyle = useMemo(() => { return { display: endpointsExist ? 'flex' : 'none', maxWidth: 200 }; }, [endpointsExist]); - const refreshIsPaused = useMemo(() => { - return !endpointsExist ? false : hasSelectedEndpoint ? true : !isAutoRefreshEnabled; - }, [endpointsExist, hasSelectedEndpoint, isAutoRefreshEnabled]); + const refreshIsPaused = !endpointsExist + ? false + : hasSelectedEndpoint + ? true + : !isAutoRefreshEnabled; - const refreshInterval = useMemo(() => { - return !endpointsExist ? DEFAULT_POLL_INTERVAL : autoRefreshInterval; - }, [endpointsExist, autoRefreshInterval]); + const refreshInterval = !endpointsExist ? DEFAULT_POLL_INTERVAL : autoRefreshInterval; - const shouldShowKQLBar = useMemo(() => { - return endpointsExist && !patternsError; - }, [endpointsExist, patternsError]); + const shouldShowKQLBar = endpointsExist && !patternsError; const paginationSetup = useMemo(() => { return { @@ -465,6 +464,57 @@ export const EndpointList = () => { [dispatch] ); + const stateToDisplay: + | 'loading' + | 'policyEmptyState' + | 'policyEmptyStateWithoutFleetAccess' + | 'hostsEmptyState' + | 'endpointTable' + | 'listError' = useMemo(() => { + if (!isInitialized) { + return 'loading'; + } else if (listError) { + return 'listError'; + } else if (endpointsExist) { + return 'endpointTable'; + } else if (canReadEndpointList && !canAccessFleet) { + return 'policyEmptyStateWithoutFleetAccess'; + } else if (!policyItemsLoading && hasPolicyData) { + return 'hostsEmptyState'; + } else { + return 'policyEmptyState'; + } + }, [ + canAccessFleet, + canReadEndpointList, + endpointsExist, + hasPolicyData, + isInitialized, + listError, + policyItemsLoading, + ]); + + const referencedAgentPolicyIds: string[] = useMemo( + // Agent Policy IDs should be unique as one Agent Policy can have only one Defend integration + () => policyItems.flatMap((item) => item.policy_ids), + [policyItems] + ); + + const { data: referencedAgentPolicies, isLoading: isAgentPolicesLoading } = + useBulkGetAgentPolicies({ + isEnabled: stateToDisplay === 'hostsEmptyState', + policyIds: referencedAgentPolicyIds, + }); + + const agentPolicyNameMap = useMemo( + () => + referencedAgentPolicies?.reduce>((acc, policy) => { + acc[policy.id] = policy.name; + return acc; + }, {}) ?? {}, + [referencedAgentPolicies] + ); + // Used for an auto-refresh super date picker version without any date/time selection const onTimeChange = useCallback(() => {}, []); @@ -526,86 +576,92 @@ export const EndpointList = () => { ); const mutableListData = useMemo(() => [...listData], [listData]); + const renderTableOrEmptyState = useMemo(() => { - if (!isInitialized) { - return ( - - } - title={ -

- {i18n.translate('xpack.securitySolution.endpoint.list.loadingEndpointManagement', { - defaultMessage: 'Loading Endpoint Management', - })} -

- } + switch (stateToDisplay) { + case 'loading': + return ( + + } + title={ +

+ {i18n.translate( + 'xpack.securitySolution.endpoint.list.loadingEndpointManagement', + { + defaultMessage: 'Loading Endpoint Management', + } + )} +

+ } + /> +
+ ); + case 'listError': + return ( + + {listError?.error}} + body={

{listError?.message}

} + /> +
+ ); + case 'endpointTable': + return ( + -
- ); - } else if (listError) { - return ( - - {listError.error}} - body={

{listError.message}

} + ); + case 'policyEmptyStateWithoutFleetAccess': + return ( + + + + ); + case 'hostsEmptyState': + const selectionOptions: EuiSelectableProps['options'] = policyItems.flatMap((policy) => + // displaying Package Policy - Agent Policy pairs + policy.policy_ids.map((agentPolicyId) => ({ + key: agentPolicyId, + label: `${policy.name} - ${agentPolicyNameMap[agentPolicyId] || agentPolicyId}`, + checked: selectedPolicyId === agentPolicyId ? 'on' : undefined, + })) + ); + + return ( + -
- ); - } else if (endpointsExist) { - return ( - - ); - } else if (canReadEndpointList && !canAccessFleet) { - return ( - - - - ); - } else if (!policyItemsLoading && hasPolicyData) { - const selectionOptions: EuiSelectableProps['options'] = policyItems - .filter((item) => item.policy_id) - .map((item) => { - return { - key: item.policy_id as string, - label: item.name, - checked: selectedPolicyId === item.policy_id ? 'on' : undefined, - }; - }); - return ( - - ); - } else { - return ( - - - - ); + ); + case 'policyEmptyState': + default: + return ( + + + + ); } }, [ - isInitialized, + stateToDisplay, listError, - endpointsExist, - canReadEndpointList, - canAccessFleet, policyItemsLoading, - hasPolicyData, mutableListData, columns, paginationSetup, @@ -615,6 +671,8 @@ export const EndpointList = () => { sorting, endpointPrivilegesLoading, policyItems, + agentPolicyNameMap, + isAgentPolicesLoading, handleDeployEndpointsClick, selectedPolicyId, handleSelectableOnChange, diff --git a/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts b/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts index 34ccf5677d1445..0e823c985c6967 100644 --- a/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts @@ -7,13 +7,14 @@ import type { QueryObserverResult, UseQueryOptions } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; import type { IHttpFetchError } from '@kbn/core-http-browser'; -import type { GetInfoResponse } from '@kbn/fleet-plugin/common'; +import type { BulkGetAgentPoliciesResponse } from '@kbn/fleet-plugin/common'; +import { type GetInfoResponse } from '@kbn/fleet-plugin/common'; import { firstValueFrom } from 'rxjs'; import type { IKibanaSearchResponse } from '@kbn/search-types'; import { ENDPOINT_PACKAGE_POLICIES_STATS_STRATEGY } from '../../../../common/endpoint/constants'; import { useHttp, useKibana } from '../../../common/lib/kibana'; import { MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../common/constants'; -import { sendGetEndpointSecurityPackage } from './ingest'; +import { sendBulkGetAgentPolicies, sendGetEndpointSecurityPackage } from './ingest'; import type { GetPolicyListResponse } from '../../pages/policy/types'; import { sendGetEndpointSpecificPackagePolicies } from './policies'; import type { ServerApiError } from '../../../common/types'; @@ -83,3 +84,23 @@ export function useGetEndpointSecurityPackage({ customQueryOptions ); } + +export function useBulkGetAgentPolicies({ + isEnabled, + policyIds, +}: { + isEnabled: boolean; + policyIds: string[]; +}): QueryObserverResult { + const http = useHttp(); + + return useQuery( + ['agentPolicies', policyIds], + + async () => { + return (await sendBulkGetAgentPolicies({ http, requestBody: { ids: policyIds } }))?.items; + }, + + { enabled: isEnabled, refetchOnWindowFocus: false, retry: 1 } + ); +} diff --git a/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts b/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts index c125464bffdb9e..2437e3d267a11d 100644 --- a/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts +++ b/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts @@ -10,9 +10,12 @@ import type { GetAgentStatusResponse, GetPackagePoliciesResponse, GetInfoResponse, + BulkGetAgentPoliciesResponse, } from '@kbn/fleet-plugin/common'; -import { epmRouteService, API_VERSIONS } from '@kbn/fleet-plugin/common'; +import { epmRouteService, API_VERSIONS, agentPolicyRouteService } from '@kbn/fleet-plugin/common'; +import type { BulkGetAgentPoliciesRequestSchema } from '@kbn/fleet-plugin/server/types'; +import type { TypeOf } from '@kbn/config-schema'; import type { NewPolicyData } from '../../../../common/endpoint/types'; import type { GetPolicyResponse, UpdatePolicyResponse } from '../../pages/policy/types'; @@ -120,3 +123,15 @@ export const sendGetEndpointSecurityPackage = async ( } return endpointPackageInfo; }; + +export const sendBulkGetAgentPolicies = async ({ + http, + requestBody, +}: { + http: HttpStart; + requestBody: TypeOf; +}): Promise => + http.post(agentPolicyRouteService.getBulkGetPath(), { + version: API_VERSIONS.public.v1, + body: JSON.stringify(requestBody), + }); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 0dd19cd14cf5e2..a0a863eec7828e 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -37777,7 +37777,6 @@ "xpack.securitySolution.endpoint.list.loadingPolicies": "Chargement des intégrations", "xpack.securitySolution.endpoint.list.noEndpointsInstructions": "Vous avez ajouté l'intégration Elastic Defend. Vous pouvez maintenant enregistrer vos agents en suivant la procédure ci-dessous.", "xpack.securitySolution.endpoint.list.noEndpointsPrompt": "Étape suivante : Enregistrer un agent avec Elastic Defend", - "xpack.securitySolution.endpoint.list.noPolicies": "Il n'existe aucune intégration.", "xpack.securitySolution.endpoint.list.os": "Système d'exploitation", "xpack.securitySolution.endpoint.list.pageSubTitle": "Hôtes exécutant Elastic Defend", "xpack.securitySolution.endpoint.list.pageTitle": "Points de terminaison", @@ -47722,4 +47721,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "Ce champ est requis.", "xpack.watcher.watcherDescription": "Détectez les modifications survenant dans vos données en créant, gérant et monitorant des alertes." } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index efad84a1470d01..ba7cc23d4aeae6 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -37519,7 +37519,6 @@ "xpack.securitySolution.endpoint.list.loadingPolicies": "統合を読み込んでいます", "xpack.securitySolution.endpoint.list.noEndpointsInstructions": "Elastic Defend統合を追加しました。次の手順を使用して、エージェントを登録してください。", "xpack.securitySolution.endpoint.list.noEndpointsPrompt": "次のステップ:Elastic Defendにエージェントを登録する", - "xpack.securitySolution.endpoint.list.noPolicies": "統合はありません。", "xpack.securitySolution.endpoint.list.os": "OS", "xpack.securitySolution.endpoint.list.pageSubTitle": "Elastic Defendを実行しているホスト", "xpack.securitySolution.endpoint.list.pageTitle": "エンドポイント", @@ -47464,4 +47463,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4024b7a1821324..68d15c252f33d7 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -37565,7 +37565,6 @@ "xpack.securitySolution.endpoint.list.loadingPolicies": "正在加载集成", "xpack.securitySolution.endpoint.list.noEndpointsInstructions": "您已添加 Elastic Defend 集成。现在,按照以下步骤注册您的代理。", "xpack.securitySolution.endpoint.list.noEndpointsPrompt": "下一步:将代理注册到 Elastic Defend", - "xpack.securitySolution.endpoint.list.noPolicies": "没有集成。", "xpack.securitySolution.endpoint.list.os": "OS", "xpack.securitySolution.endpoint.list.pageSubTitle": "运行 Elastic Defend 的主机", "xpack.securitySolution.endpoint.list.pageTitle": "终端", @@ -47518,4 +47517,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file