Skip to content

Commit

Permalink
[Fleet] Show multiple agent policies in integrations table (#186087)
Browse files Browse the repository at this point in the history
Closes #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](#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](https://github.com/elastic/kibana/assets/16084106/bd8a4e6a-a752-46bb-8003-a4e875d0fa93)

![Screenshot 2024-06-12 at 15 46
38](https://github.com/elastic/kibana/assets/16084106/f93a91bc-bae7-40a0-8425-ac2dbbcaeae4)

When one of the policies is managed:
![Screenshot 2024-06-13 at 11 30
01](https://github.com/elastic/kibana/assets/16084106/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](https://github.com/elastic/kibana/assets/16084106/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 <[email protected]>
  • Loading branch information
criamico and kibanamachine authored Jun 14, 2024
1 parent 780fda1 commit ee15561
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -35,6 +36,7 @@ import {
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants';
import {
AgentEnrollmentFlyout,
MultipleAgentPoliciesSummaryLine,
AgentPolicySummaryLine,
PackagePolicyActionsMenu,
} from '../../../../../components';
Expand Down Expand Up @@ -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,
Expand All @@ -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[];
Expand Down Expand Up @@ -167,7 +172,8 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
},
[setPagination]
);

const canShowMultiplePoliciesCell =
enableReusableIntegrationPolicies && canReadIntegrationPolicies && canReadAgentPolicies;
const columns: Array<EuiTableFieldDataColumnType<InMemoryPackagePolicyAndAgentPolicy>> = useMemo(
() => [
{
Expand Down Expand Up @@ -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 />
);
Expand Down Expand Up @@ -313,8 +322,9 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
[
getHref,
canWriteIntegrationPolicies,
canAddAgents,
canShowMultiplePoliciesCell,
canAddFleetServers,
canAddAgents,
showAddAgentHelpForPackagePolicyId,
]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/fleet/public/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ 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';
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';
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -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>
);
});

0 comments on commit ee15561

Please sign in to comment.