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 c6ed4d80a9d81..9f49db0cfb5cc 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 @@ -21,6 +21,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedRelative, FormattedMessage } from '@kbn/i18n-react'; import { policyHasFleetServer } from '../../../../../../../../common/services'; +import { ExperimentalFeaturesService } from '../../../../../services'; import { InstallStatus } from '../../../../../types'; import type { GetAgentPoliciesResponseItem, InMemoryPackagePolicy } from '../../../../../types'; @@ -35,6 +36,7 @@ import { import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; import { AgentEnrollmentFlyout, + MultipleAgentPoliciesSummaryLine, AgentPolicySummaryLine, PackagePolicyActionsMenu, } from '../../../../../components'; @@ -101,6 +103,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps const getPackageInstallStatus = useGetPackageInstallStatus(); const packageInstallStatus = getPackageInstallStatus(name); const { pagination, pageSizeOptions, setPagination } = useUrlPagination(); + const { enableReusableIntegrationPolicies } = ExperimentalFeaturesService.get(); const { data, @@ -114,8 +117,10 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps const { isPackagePolicyUpgradable } = useIsPackagePolicyUpgradable(); 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[]; @@ -167,7 +172,8 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps }, [setPagination] ); - + const canShowMultiplePoliciesCell = + enableReusableIntegrationPolicies && canReadIntegrationPolicies && canReadAgentPolicies; const columns: Array> = useMemo( () => [ { @@ -228,8 +234,11 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps truncateText: true, render(id, { agentPolicies }) { return agentPolicies.length > 0 ? ( - // TODO: handle multiple agent policies - + canShowMultiplePoliciesCell && agentPolicies.length > 1 ? ( + + ) : ( + + ) ) : ( ); @@ -313,8 +322,9 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps [ getHref, canWriteIntegrationPolicies, - canAddAgents, + canShowMultiplePoliciesCell, canAddFleetServers, + canAddAgents, showAddAgentHelpForPackagePolicyId, ] ); diff --git a/x-pack/plugins/fleet/public/components/link_and_revision.test.tsx b/x-pack/plugins/fleet/public/components/agent_policy_summary_line.test.tsx similarity index 95% rename from x-pack/plugins/fleet/public/components/link_and_revision.test.tsx rename to x-pack/plugins/fleet/public/components/agent_policy_summary_line.test.tsx index 46fdfe69c63bd..e398851e8892e 100644 --- a/x-pack/plugins/fleet/public/components/link_and_revision.test.tsx +++ b/x-pack/plugins/fleet/public/components/agent_policy_summary_line.test.tsx @@ -12,7 +12,7 @@ import { createFleetTestRendererMock } from '../mock'; import type { AgentPolicy, Agent } from '../types'; -import { AgentPolicySummaryLine } from './link_and_revision'; +import { AgentPolicySummaryLine } from './agent_policy_summary_line'; describe('AgentPolicySummaryLine', () => { let testRenderer: TestRenderer; diff --git a/x-pack/plugins/fleet/public/components/link_and_revision.tsx b/x-pack/plugins/fleet/public/components/agent_policy_summary_line.tsx similarity index 99% rename from x-pack/plugins/fleet/public/components/link_and_revision.tsx rename to x-pack/plugins/fleet/public/components/agent_policy_summary_line.tsx index 5d0f0070f6668..b055b51728a43 100644 --- a/x-pack/plugins/fleet/public/components/link_and_revision.tsx +++ b/x-pack/plugins/fleet/public/components/agent_policy_summary_line.tsx @@ -26,7 +26,6 @@ export const AgentPolicySummaryLine = memo<{ const { name, id, is_managed: isManaged, description } = policy; const revision = agent ? agent.policy_revision : policy.revision; - return ( diff --git a/x-pack/plugins/fleet/public/components/index.ts b/x-pack/plugins/fleet/public/components/index.ts index 3b076bd8c18a7..5a995ab1ecbce 100644 --- a/x-pack/plugins/fleet/public/components/index.ts +++ b/x-pack/plugins/fleet/public/components/index.ts @@ -20,7 +20,7 @@ export { PackagePolicyDeleteProvider } from './package_policy_delete_provider'; export { PackagePolicyActionsMenu } from './package_policy_actions_menu'; export { AddAgentHelpPopover } from './add_agent_help_popover'; export { EuiButtonWithTooltip } from './eui_button_with_tooltip'; -export * from './link_and_revision'; +export * from './agent_policy_summary_line'; export * from './agent_enrollment_flyout'; export * from './platform_selector'; export { ConfirmForceInstallModal } from './confirm_force_install_modal'; @@ -28,3 +28,4 @@ export { DevtoolsRequestFlyoutButton } from './devtools_request_flyout'; export { HeaderReleaseBadge, InlineReleaseBadge } from './release_badge'; export { WithGuidedOnboardingTour } from './with_guided_onboarding_tour'; export { UninstallCommandFlyout } from './uninstall_command_flyout'; +export { MultipleAgentPoliciesSummaryLine } from './multiple_agent_policy_summary_line'; diff --git a/x-pack/plugins/fleet/public/components/multiple_agent_policy_summary_line.test.tsx b/x-pack/plugins/fleet/public/components/multiple_agent_policy_summary_line.test.tsx new file mode 100644 index 0000000000000..0d88dcc4b44b7 --- /dev/null +++ b/x-pack/plugins/fleet/public/components/multiple_agent_policy_summary_line.test.tsx @@ -0,0 +1,54 @@ +/* + * 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 { act, fireEvent } from '@testing-library/react'; +import React from 'react'; + +import type { TestRenderer } from '../mock'; +import { createFleetTestRendererMock } from '../mock'; + +import type { AgentPolicy } from '../types'; + +import { MultipleAgentPoliciesSummaryLine } from './multiple_agent_policy_summary_line'; + +describe('MultipleAgentPolicySummaryLine', () => { + let testRenderer: TestRenderer; + + const render = (agentPolicies: AgentPolicy[]) => + testRenderer.render(); + + beforeEach(() => { + testRenderer = createFleetTestRendererMock(); + }); + + test('it should render only the policy name when there is only one policy', async () => { + const results = render([{ name: 'Test policy', revision: 2 }] as AgentPolicy[]); + expect(results.container.textContent).toBe('Test policy'); + expect(results.queryByTestId('agentPolicyNameBadge')).toBeInTheDocument(); + expect(results.queryByTestId('agentPoliciesNumberBadge')).not.toBeInTheDocument(); + }); + + test('it should render the first policy name and the badge when there are multiple policies', async () => { + const results = render([ + { name: 'Test policy 1', id: '0001' }, + { name: 'Test policy 2', id: '0002' }, + { name: 'Test policy 3', id: '0003' }, + ] as AgentPolicy[]); + expect(results.queryByTestId('agentPolicyNameBadge')).toBeInTheDocument(); + expect(results.queryByTestId('agentPoliciesNumberBadge')).toBeInTheDocument(); + expect(results.container.textContent).toBe('Test policy 1+2'); + + await act(async () => { + fireEvent.click(results.getByTestId('agentPoliciesNumberBadge')); + }); + expect(results.queryByTestId('agentPoliciesPopover')).toBeInTheDocument(); + expect(results.queryByTestId('agentPoliciesPopoverButton')).toBeInTheDocument(); + expect(results.queryByTestId('policy-0001')).toBeInTheDocument(); + expect(results.queryByTestId('policy-0002')).toBeInTheDocument(); + expect(results.queryByTestId('policy-0003')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/fleet/public/components/multiple_agent_policy_summary_line.tsx b/x-pack/plugins/fleet/public/components/multiple_agent_policy_summary_line.tsx new file mode 100644 index 0000000000000..2a869f12bd817 --- /dev/null +++ b/x-pack/plugins/fleet/public/components/multiple_agent_policy_summary_line.tsx @@ -0,0 +1,127 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiPopover, + EuiPopoverTitle, + EuiPopoverFooter, + EuiButton, + EuiListGroup, + type EuiListGroupItemProps, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { CSSProperties } from 'react'; +import { useMemo } from 'react'; +import React, { memo, useState } from 'react'; + +import type { AgentPolicy } from '../../common/types'; +import { useLink } from '../hooks'; +const MIN_WIDTH: CSSProperties = { minWidth: 0 }; + +export const MultipleAgentPoliciesSummaryLine = memo<{ + policies: AgentPolicy[]; + direction?: 'column' | 'row'; +}>(({ policies, direction = 'row' }) => { + const { getHref } = useLink(); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const closePopover = () => setIsPopoverOpen(false); + + // as default, show only the first policy + const policy = policies[0]; + const { name, id } = policy; + + const listItems: EuiListGroupItemProps[] = useMemo(() => { + return policies.map((p) => { + return { + 'data-test-subj': `policy-${p.id}`, + label: p.name || p.id, + href: getHref('policy_details', { policyId: p.id }), + iconType: 'dot', + extraAction: { + color: 'text', + iconType: p.is_managed ? 'lock' : '', + alwaysShow: !!p.is_managed, + iconSize: 's', + 'aria-label': 'Hosted agent policy', + }, + showToolTip: !!p.is_managed, + toolTipText: i18n.translate('xpack.fleet.agentPolicySummaryLine.hostedPolicyTooltip', { + defaultMessage: + 'This policy is managed outside of Fleet. Most actions related to this policy are unavailable.', + }), + }; + }); + }, [getHref, policies]); + + return ( + + + + + + + + {name || id} + + + {policies.length > 1 && ( + + setIsPopoverOpen(!isPopoverOpen)} + onClickAriaLabel="Open agent policies popover" + > + {`+${policies.length - 1}`} + + + + {i18n.translate('xpack.fleet.agentPolicySummaryLine.popover.title', { + defaultMessage: 'This integration is shared by', + })} + +
+ +
+ + {/* TODO: implement missing onClick function */} + + {i18n.translate('xpack.fleet.agentPolicySummaryLine.popover.button', { + defaultMessage: 'Manage agent policies', + })} + + +
+
+ )} +
+
+
+
+
+ ); +});