[Fleet] Show multiple agent policies in integrations table (#186087)

Closes https://github.com/elastic/kibana/issues/182111

## Summary
 Show multiple agent policies in integrations table.

## Testing
- Enable feature flag `enableReusableIntegrationPolicies`
- Install an integration that has more than one agent policies
associated with it (instructions are
[here](https://github.com/elastic/kibana/pull/185916))
- Navigate to integrations table and verify that the policy displays a
badge with the number of associated policies -1 and that it opens up a
popover, like in below screenshots.

**NOTE** the button "Manage agent policies" does not work for now, as
the feature is under development and it's hidden with a feature flag!

### With feature flag enabled, when integration has multiple agent
policies

![Screenshot 2024-06-12 at 15 49
34](bd8a4e6a-a752-46bb-8003-a4e875d0fa93)

![Screenshot 2024-06-12 at 15 46
38](f93a91bc-bae7-40a0-8425-ac2dbbcaeae4)

When one of the policies is managed:
![Screenshot 2024-06-13 at 11 30
01](3ba0d5cb-4af1-46e5-875a-d4391c79ad6d)


### When feature flag not enabled or integration has only one agent
policy

The UI remains as it is today:
![Screenshot 2024-06-12 at 15 48
56](14122ad3-d4f8-448a-b4b3-6f08900ba833)


### Checklist

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cristina Amico 2024-06-14 11:35:56 +02:00 committed by GitHub
parent 780fda17c1
commit ee15561217
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 198 additions and 7 deletions

View file

@ -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<EuiTableFieldDataColumnType<InMemoryPackagePolicyAndAgentPolicy>> = 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
<AgentPolicySummaryLine policy={agentPolicies[0]} />
canShowMultiplePoliciesCell && agentPolicies.length > 1 ? (
<MultipleAgentPoliciesSummaryLine policies={agentPolicies} />
) : (
<AgentPolicySummaryLine policy={agentPolicies[0]} />
)
) : (
<AgentPolicyNotFound />
);
@ -313,8 +322,9 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
[
getHref,
canWriteIntegrationPolicies,
canAddAgents,
canShowMultiplePoliciesCell,
canAddFleetServers,
canAddAgents,
showAddAgentHelpForPackagePolicyId,
]
);

View file

@ -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;

View file

@ -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 (
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiFlexItem>

View file

@ -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';

View file

@ -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(<MultipleAgentPoliciesSummaryLine policies={agentPolicies} />);
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();
});
});

View file

@ -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 (
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiFlexItem>
<EuiFlexGroup
direction={direction}
gutterSize={direction === 'column' ? 'none' : 's'}
alignItems="baseline"
style={MIN_WIDTH}
responsive={false}
justifyContent={'flexStart'}
>
<EuiFlexItem grow={false} className="eui-textTruncate">
<EuiFlexGroup style={MIN_WIDTH} gutterSize="s" alignItems="baseline" responsive={false}>
<EuiFlexItem grow={false} className="eui-textTruncate">
<EuiBadge color="default" data-test-subj="agentPolicyNameBadge">
{name || id}
</EuiBadge>
</EuiFlexItem>
{policies.length > 1 && (
<EuiFlexItem grow={false}>
<EuiBadge
color="hollow"
data-test-subj="agentPoliciesNumberBadge"
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
onClickAriaLabel="Open agent policies popover"
>
{`+${policies.length - 1}`}
</EuiBadge>
<EuiPopover
data-test-subj="agentPoliciesPopover"
isOpen={isPopoverOpen}
closePopover={closePopover}
anchorPosition="downCenter"
>
<EuiPopoverTitle>
{i18n.translate('xpack.fleet.agentPolicySummaryLine.popover.title', {
defaultMessage: 'This integration is shared by',
})}
</EuiPopoverTitle>
<div style={{ width: '280px' }}>
<EuiListGroup
listItems={listItems}
color="primary"
size="s"
gutterSize="none"
/>
</div>
<EuiPopoverFooter>
{/* TODO: implement missing onClick function */}
<EuiButton fullWidth size="s" data-test-subj="agentPoliciesPopoverButton">
{i18n.translate('xpack.fleet.agentPolicySummaryLine.popover.button', {
defaultMessage: 'Manage agent policies',
})}
</EuiButton>
</EuiPopoverFooter>
</EuiPopover>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
});