diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.test.tsx index c1bba8284548a..6531d2e72e098 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.test.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.test.tsx @@ -7,85 +7,194 @@ import React from 'react'; -import { act } from '@testing-library/react'; +import { act, fireEvent } from '@testing-library/react'; import { createIntegrationsTestRendererMock } from '../../../../../../../../mock'; import type { AgentPolicy } from '../../../../../../types'; +import { useAuthz, useMultipleAgentPolicies } from '../../../../../../hooks'; import { PackagePolicyAgentsCell } from './package_policy_agents_cell'; +jest.mock('../../../../../../hooks', () => ({ + ...jest.requireActual('../../../../../../hooks'), + useAuthz: jest.fn(), + useMultipleAgentPolicies: jest.fn(), + useConfirmForceInstall: jest.fn(), +})); + +const useMultipleAgentPoliciesMock = useMultipleAgentPolicies as jest.MockedFunction< + typeof useMultipleAgentPolicies +>; function renderCell({ - agentCount = 0, - agentPolicy = {} as AgentPolicy, + agentPolicies = [] as AgentPolicy[], onAddAgent = () => {}, hasHelpPopover = false, - canAddAgents = true, }) { const renderer = createIntegrationsTestRendererMock(); return renderer.render( ); } describe('PackagePolicyAgentsCell', () => { - test('it should display add agent if count is 0', async () => { - const utils = renderCell({ agentCount: 0 }); - await act(async () => { - expect(utils.queryByText('Add agent')).toBeInTheDocument(); - }); + beforeEach(() => { + jest.mocked(useAuthz).mockReturnValue({ + fleet: { + addAgents: true, + addFleetServers: true, + }, + } as any); }); - test('it should not display add agent if policy is managed', async () => { - const utils = renderCell({ - agentCount: 0, - agentPolicy: { - is_managed: true, - } as AgentPolicy, + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('when multiple agent policies is disabled', () => { + beforeEach(() => { + useMultipleAgentPoliciesMock.mockReturnValue({ canUseMultipleAgentPolicies: false }); }); - await act(async () => { - expect(utils.queryByText('Add agent')).not.toBeInTheDocument(); + + test('it should display add agent button if count is 0', async () => { + const utils = renderCell({ + agentPolicies: [ + { + name: 'test Policy 1', + } as AgentPolicy, + ], + }); + utils.debug(); + await act(async () => { + expect(utils.queryByText('Add agent')).toBeInTheDocument(); + }); }); - }); - test('it should display only count if count > 0', async () => { - const utils = renderCell({ agentCount: 9999 }); - await act(async () => { - expect(utils.queryByText('Add agent')).not.toBeInTheDocument(); - expect(utils.queryByText('9999')).toBeInTheDocument(); + test('it should display only count if count > 0', async () => { + const utils = renderCell({ + agentPolicies: [ + { + name: 'test Policy 1', + agents: 999, + } as AgentPolicy, + ], + }); + await act(async () => { + expect(utils.queryByText('Add agent')).not.toBeInTheDocument(); + expect(utils.queryByText('999')).toBeInTheDocument(); + }); }); - }); - test('it should display help popover if count is 0 and hasHelpPopover=true', async () => { - const utils = renderCell({ agentCount: 0, hasHelpPopover: true }); - await act(async () => { - expect(utils.queryByText('9999')).not.toBeInTheDocument(); - expect(utils.queryByText('Add agent')).toBeInTheDocument(); - expect( - utils.container.querySelector('[data-test-subj="addAgentHelpPopover"]') - ).toBeInTheDocument(); + test('it should not display help popover if count is > 0 and hasHelpPopover=true', async () => { + const utils = renderCell({ + agentPolicies: [ + { + name: 'test Policy 1', + agents: 999, + } as AgentPolicy, + ], + hasHelpPopover: true, + }); + await act(async () => { + expect(utils.queryByText('999')).toBeInTheDocument(); + expect(utils.queryByText('Add agent')).not.toBeInTheDocument(); + expect( + utils.container.querySelector('[data-test-subj="addAgentHelpPopover"]') + ).not.toBeInTheDocument(); + }); }); - }); - test('it should not display help popover if count is > 0 and hasHelpPopover=true', async () => { - const utils = renderCell({ agentCount: 9999, hasHelpPopover: true }); - await act(async () => { - expect(utils.queryByText('9999')).toBeInTheDocument(); - expect(utils.queryByText('Add agent')).not.toBeInTheDocument(); - expect( - utils.container.querySelector('[data-test-subj="addAgentHelpPopover"]') - ).not.toBeInTheDocument(); + + test('it should display help popover if count = 0 and hasHelpPopover=true', async () => { + const utils = renderCell({ + hasHelpPopover: true, + agentPolicies: [ + { + name: 'test Policy 1', + } as AgentPolicy, + ], + }); + await act(async () => { + expect(utils.queryByText('9999')).not.toBeInTheDocument(); + expect(utils.queryByText('Add agent')).toBeInTheDocument(); + expect( + utils.container.querySelector('[data-test-subj="addAgentHelpPopover"]') + ).toBeInTheDocument(); + }); + }); + + test('it should not display add agent button if policy is managed', async () => { + const utils = renderCell({ + agentPolicies: [ + { + name: 'test Policy 1', + agents: 999, + is_managed: true, + } as AgentPolicy, + ], + }); + utils.debug(); + await act(async () => { + expect(utils.queryByText('Add agent')).not.toBeInTheDocument(); + expect(utils.queryByTestId('LinkedAgentCountLink')).toBeInTheDocument(); + expect(utils.queryByText('999')).toBeInTheDocument(); + }); + }); + + test('Add agent button should be disabled if canAddAgents is false', async () => { + jest.mocked(useAuthz).mockReturnValue({ + fleet: { + addAgents: false, + }, + } as any); + + const utils = renderCell({ + agentPolicies: [ + { + name: 'test Policy 1', + } as AgentPolicy, + ], + }); + await act(async () => { + expect(utils.container.querySelector('[data-test-subj="addAgentButton"]')).toBeDisabled(); + }); }); }); - test('it should be disabled if canAddAgents is false', async () => { - const utils = renderCell({ agentCount: 0, canAddAgents: false }); - await act(async () => { - expect(utils.container.querySelector('[data-test-subj="addAgentButton"]')).toBeDisabled(); + + describe('when multiple agent policies is enabled', () => { + beforeEach(() => { + useMultipleAgentPoliciesMock.mockReturnValue({ canUseMultipleAgentPolicies: true }); + }); + + test('it should display agent count sum and popover if agent count > 0', async () => { + jest.mocked(useAuthz).mockReturnValue({ + fleet: { + addAgents: false, + }, + } as any); + + const utils = renderCell({ + agentPolicies: [ + { + name: 'test Policy 1', + agents: 100, + } as AgentPolicy, + { + name: 'test Policy 2', + agents: 200, + } as AgentPolicy, + ], + }); + await act(async () => { + expect(utils.queryByText('300')).toBeInTheDocument(); + expect(utils.queryByText('Add agent')).not.toBeInTheDocument(); + const button = utils.getByTestId('agentsCountsButton'); + fireEvent.click(button); + expect(utils.queryByTestId('agentCountsPopover')).toBeInTheDocument(); + }); }); }); }); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.tsx index 1ede5b09cea99..35e6e8634e34a 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.tsx @@ -5,83 +5,238 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { Fragment, useState, useMemo } from 'react'; -import { EuiButton } from '@elastic/eui'; +import { + EuiButton, + EuiLink, + EuiPopover, + EuiPopoverTitle, + EuiHorizontalRule, + EuiSpacer, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiPopoverFooter, + EuiButtonEmpty, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { sortBy } from 'lodash'; import { LinkedAgentCount, AddAgentHelpPopover } from '../../../../../../components'; import type { AgentPolicy } from '../../../../../../types'; +import { policyHasFleetServer } from '../../../../../../services'; +import { useAuthz, useLink, useMultipleAgentPolicies } from '../../../../../../hooks'; +import { + PRIVILEGED_AGENT_KUERY, + UNPRIVILEGED_AGENT_KUERY, + AGENTS_PREFIX, +} from '../../../../../../constants'; const AddAgentButton = ({ onAddAgent, canAddAgents, + withPopover, }: { onAddAgent: () => void; canAddAgents: boolean; -}) => ( - - - -); - -const AddAgentButtonWithPopover = ({ - onAddAgent, - canAddAgents, -}: { - onAddAgent: () => void; - canAddAgents: boolean; + withPopover?: boolean; }) => { const [isHelpOpen, setIsHelpOpen] = useState(true); const onAddAgentCloseHelp = () => { setIsHelpOpen(false); onAddAgent(); }; - const button = ; - return ( + return withPopover ? ( + + + } isOpen={isHelpOpen} closePopover={() => setIsHelpOpen(false)} /> + ) : ( + + + ); }; export const PackagePolicyAgentsCell = ({ - agentPolicy, - agentCount = 0, + agentPolicies, onAddAgent, hasHelpPopover = false, - canAddAgents, }: { - agentPolicy: AgentPolicy; - agentCount?: number; + agentPolicies: AgentPolicy[]; hasHelpPopover?: boolean; onAddAgent: () => void; - canAddAgents: boolean; }) => { - if (agentCount > 0 || agentPolicy.is_managed) { - return ( - - ); + const { canUseMultipleAgentPolicies } = useMultipleAgentPolicies(); + + const agentCount = agentPolicies.reduce((acc, curr) => { + return (acc += curr?.agents || 0); + }, 0); + + const canAddAgents = useAuthz().fleet.addAgents; + const canAddFleetServers = useAuthz().fleet.addFleetServers; + + if (canUseMultipleAgentPolicies && agentCount > 0 && agentPolicies.length > 1) { + return ; } - if (!hasHelpPopover || !canAddAgents) { - return ; + if (!canUseMultipleAgentPolicies || (agentCount > 0 && agentPolicies.length === 1)) { + const agentPolicy = agentPolicies[0]; + const canAddAgentsForPolicy = policyHasFleetServer(agentPolicy) + ? canAddFleetServers + : canAddAgents; + if (agentCount > 0 || agentPolicy.is_managed) + return ( + + ); + else { + ; + } } + return ( + + ); +}; + +export const AgentsCountBreakDown = ({ + agentPolicies, + agentCount, + privilegeMode, +}: { + agentPolicies: AgentPolicy[]; + agentCount: number; + privilegeMode?: 'privileged' | 'unprivileged'; +}) => { + const { getHref } = useLink(); + const authz = useAuthz(); + + const canReadAgents = authz.fleet.readAgents; - return ; + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const closePopover = () => setIsPopoverOpen(false); + + const getKuery = (agentPolicyId: string) => + `${AGENTS_PREFIX}.policy_id : "${agentPolicyId}"${ + privilegeMode + ? ` and ${ + privilegeMode === 'unprivileged' ? UNPRIVILEGED_AGENT_KUERY : PRIVILEGED_AGENT_KUERY + }` + : '' + }`; + const topFivePolicies = useMemo( + () => sortBy(agentPolicies, 'agents').reverse().slice(0, 5), + [agentPolicies] + ); + + return ( + <> + setIsPopoverOpen(!isPopoverOpen)} + data-test-subj="agentsCountsButton" + > + {agentCount} + + } + > + + {i18n.translate('xpack.fleet.agentsCountsBreakdown.popover.title', { + defaultMessage: 'Agents breakdown', + })} + +
+ + + {i18n.translate('xpack.fleet.agentsCountsBreakdown.popover.heading', { + defaultMessage: 'Top values', + })} + + + + {topFivePolicies.map((agentPolicy) => ( + + + + + {agentPolicy.name} + + + + {agentPolicy?.agents && agentPolicy.agents > 0 ? ( + + {agentPolicy.agents} + + ) : ( + 0 + )} + + + + + ))} + + {agentCount > 0 ? ( + + + {i18n.translate('xpack.fleet.agentsCountsBreakdown.popover.button', { + defaultMessage: 'View all {agentCount, plural, one {# agent} other {# agents}}', + values: { agentCount }, + })} + + + ) : null} +
+
+ + ); }; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx index b475c4d39d767..300de597f6900 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx @@ -20,8 +20,6 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedRelative, FormattedMessage } from '@kbn/i18n-react'; -import { policyHasFleetServer } from '../../../../../../../../common/services'; - import { InstallStatus } from '../../../../../types'; import type { GetAgentPoliciesResponseItem, InMemoryPackagePolicy } from '../../../../../types'; import { @@ -54,6 +52,7 @@ interface PackagePoliciesPanelProps { interface InMemoryPackagePolicyAndAgentPolicy { packagePolicy: InMemoryPackagePolicy; agentPolicies: GetAgentPoliciesResponseItem[]; + rowIndex: number; } const IntegrationDetailsLink = memo<{ @@ -89,6 +88,8 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps const [flyoutOpenForPolicyId, setFlyoutOpenForPolicyId] = useState( agentPolicyIdFromParams ); + const [selectedTableIndex, setSelectedTableIndex] = useState(); + const { getPath, getHref } = useLink(); const getPackageInstallStatus = useGetPackageInstallStatus(); const packageInstallStatus = getPackageInstallStatus(name); @@ -108,19 +109,18 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps const canWriteIntegrationPolicies = useAuthz().integrations.writeIntegrationPolicies; const canReadIntegrationPolicies = useAuthz().integrations.readIntegrationPolicies; - const canAddAgents = useAuthz().fleet.addAgents; - const canAddFleetServers = useAuthz().fleet.addFleetServers; const canReadAgentPolicies = useAuthz().fleet.readAgentPolicies; const packageAndAgentPolicies = useMemo((): Array<{ agentPolicies: GetAgentPoliciesResponseItem[]; packagePolicy: InMemoryPackagePolicy; + rowIndex: number; }> => { if (!data?.items) { return []; } - const newPolicies = data.items.map(({ agentPolicies, packagePolicy }) => { + const newPolicies = data.items.map(({ agentPolicies, packagePolicy }, index) => { const hasUpgrade = isPackagePolicyUpgradable(packagePolicy); return { @@ -129,6 +129,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps ...packagePolicy, hasUpgrade, }, + rowIndex: index, }; }); @@ -283,7 +284,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentCount', { defaultMessage: 'Agents', }), - render({ agentPolicies, packagePolicy }: InMemoryPackagePolicyAndAgentPolicy) { + render({ agentPolicies, packagePolicy, rowIndex }: InMemoryPackagePolicyAndAgentPolicy) { if (agentPolicies.length === 0) { return ( @@ -294,16 +295,13 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps ); } - const agentPolicy = agentPolicies[0]; // TODO: handle multiple agent policies - const canAddAgentsForPolicy = policyHasFleetServer(agentPolicy) - ? canAddFleetServers - : canAddAgents; return ( setFlyoutOpenForPolicyId(agentPolicy.id)} - canAddAgents={canAddAgentsForPolicy} + agentPolicies={agentPolicies} + onAddAgent={() => { + setSelectedTableIndex(rowIndex); + setFlyoutOpenForPolicyId(agentPolicies[0].id); + }} hasHelpPopover={showAddAgentHelpForPackagePolicyId === packagePolicy.id} /> ); @@ -340,8 +338,6 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps getHref, canWriteIntegrationPolicies, canShowMultiplePoliciesCell, - canAddFleetServers, - canAddAgents, showAddAgentHelpForPackagePolicyId, refreshPolicies, ] @@ -373,11 +369,13 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps ); } - const selectedPolicies = packageAndAgentPolicies.find(({ agentPolicies: policies }) => - policies.find((policy) => policy.id === flyoutOpenForPolicyId) - ); + + const selectedPolicies = + selectedTableIndex !== undefined ? packageAndAgentPolicies[selectedTableIndex] : undefined; + const agentPolicies = selectedPolicies?.agentPolicies; const packagePolicy = selectedPolicies?.packagePolicy; + const flyoutPolicy = agentPolicies?.length === 1 ? agentPolicies[0] : undefined; return ( @@ -402,7 +400,8 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps const { addAgentToPolicyId, ...rest } = parse(search); history.replace({ search: stringify(rest) }); }} - agentPolicy={agentPolicies[0]} + agentPolicy={flyoutPolicy} + selectedAgentPolicies={agentPolicies} isIntegrationFlow={true} installedPackagePolicy={{ name: packagePolicy?.package?.name || '', diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx index 5ccdf37951703..46323eab29b43 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx @@ -53,6 +53,7 @@ export * from './steps'; export const AgentEnrollmentFlyout: React.FunctionComponent = ({ onClose, agentPolicy, + selectedAgentPolicies, defaultMode = 'managed', isIntegrationFlow, installedPackagePolicy, @@ -69,12 +70,15 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ const [selectionType, setSelectionType] = useState(); const { - agentPolicies, + agentPolicies: fetchedAgentPolicies, isLoadingInitialAgentPolicies, isLoadingAgentPolicies, refreshAgentPolicies, } = useAgentEnrollmentFlyoutData(); + // Have the option to pass agentPolicies from props, otherwise use the fetched ones + const agentPolicies = selectedAgentPolicies ? selectedAgentPolicies : fetchedAgentPolicies; + const { agentPolicyWithPackagePolicies } = useAgentPolicyWithPackagePolicies(selectedPolicyId); const { diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts index a58feeda65617..0c2e51e294b14 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts @@ -47,7 +47,6 @@ export interface BaseProps { * The user selected policy to be used. If this value is `undefined` a value must be provided for `agentPolicies`. */ agentPolicy?: AgentPolicy; - isFleetServerPolicySelected?: boolean; isK8s?: K8sMode; @@ -65,6 +64,7 @@ export interface BaseProps { export interface FlyOutProps extends BaseProps { onClose: () => void; defaultMode?: FlyoutMode; + selectedAgentPolicies?: AgentPolicy[]; } export interface InstructionProps extends BaseProps { diff --git a/x-pack/plugins/fleet/public/components/linked_agent_count.tsx b/x-pack/plugins/fleet/public/components/linked_agent_count.tsx index 5b92ac3e96f6e..ff37287ae91ef 100644 --- a/x-pack/plugins/fleet/public/components/linked_agent_count.tsx +++ b/x-pack/plugins/fleet/public/components/linked_agent_count.tsx @@ -43,7 +43,11 @@ export const LinkedAgentCount = memo< }`; return count > 0 ? ( - + {displayValue} ) : ( diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx index 4da1711b28313..b28cf97b464a9 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx @@ -37,7 +37,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ const { getHref } = useLink(); const authz = useAuthz(); - const agentPolicy = agentPolicies.length > 0 ? agentPolicies[0] : undefined; // TODO: handle multiple agent policies + const agentPolicy = agentPolicies.length > 0 ? agentPolicies[0] : undefined; const canWriteIntegrationPolicies = authz.integrations.writeIntegrationPolicies; const isFleetServerPolicy = agentPolicy && policyHasFleetServer(agentPolicy); @@ -168,7 +168,8 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ {isEnrollmentFlyoutOpen && (