Skip to content

Commit

Permalink
[Fleet] Added modal to manage agent policies of an integration policy (
Browse files Browse the repository at this point in the history
…#186987)

## Summary

Closes #182112

Added modal to manage agent policies.

To verify:
- go to an integration where the integration policies are listed
- click on the `+` button in the agent policies column
- click on `Manage agent policies` in the popover
- add/remove agent policies in the modal
- click submit, the integration policy should be updated to be linked to
the updated agent policies



<img width="1614" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/33101f7e-8563-4990-9a52-74e5448c21da">

Made a change to the table column display to show a `+` button even if
there is only one policy. Previously the popover could only be accessed
if there are at least 2 agent policies.
Also restored the agent policy link, lock icon and revision display
(instead of a badge) if there are multiple agent policies.
@simosilvestri Let me know if you have any UX recommendation as it
differs slightly from the prototype.

<img width="982" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/51aff39c-3f84-4861-8614-c0e16b64f4bf">

<img width="1533" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/f08de084-d4c2-47c6-b532-f875ceaf10ef">

EDIT: after discussing with Simona, removed the + button in case of a
single agent policy assigned to the integration policy.
<img width="1391" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/9ce5e98a-3ea2-4b79-a073-62047012db03">


Disabling `Manage agent policies` button if the current user doesn't
have at least write integration policies and write agent policies
privilege.
This is how it looks with read privileges:
<img width="2294" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/625843fb-8a50-4a06-b3a3-dc95a9fc2654">



### Checklist

- [x] 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)
- [x] [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
  • Loading branch information
juliaElastic authored Jul 2, 2024
1 parent 369fb60 commit 813fc21
Show file tree
Hide file tree
Showing 11 changed files with 823 additions and 264 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,45 @@ import type { EuiComboBoxOptionOption } from '@elastic/eui';
import { EuiComboBox } from '@elastic/eui';

import { i18n } from '@kbn/i18n';
import { uniq } from 'lodash';

import React, { useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';

import type { PackageInfo } from '../../../../../../../../../common';
import type { AgentPolicy, PackageInfo } from '../../../../../../../../../common';

export interface Props {
isLoading: boolean;
agentPolicyMultiOptions: Array<EuiComboBoxOptionOption<string>>;
selectedPolicyIds: string[];
setSelectedPolicyIds: (policyIds: string[]) => void;
packageInfo?: PackageInfo;
selectedAgentPolicies: AgentPolicy[];
}

export const AgentPolicyMultiSelect: React.FunctionComponent<Props> = ({
isLoading,
agentPolicyMultiOptions,
selectedPolicyIds,
setSelectedPolicyIds,
selectedAgentPolicies,
}) => {
const selectedOptions = useMemo(() => {
return agentPolicyMultiOptions.filter((option) => selectedPolicyIds.includes(option.key!));
}, [agentPolicyMultiOptions, selectedPolicyIds]);

// managed policies cannot be removed
const updateSelectedPolicyIds = useCallback(
(ids: string[]) => {
setSelectedPolicyIds(
uniq([
...selectedAgentPolicies.filter((policy) => policy.is_managed).map((policy) => policy.id),
...ids,
])
);
},
[selectedAgentPolicies, setSelectedPolicyIds]
);

return (
<EuiComboBox
aria-label="Select Multiple Agent Policies"
Expand All @@ -44,9 +60,9 @@ export const AgentPolicyMultiSelect: React.FunctionComponent<Props> = ({
)}
options={agentPolicyMultiOptions}
selectedOptions={selectedOptions}
onChange={(newOptions) => {
setSelectedPolicyIds(newOptions.map((option: any) => option.key));
}}
onChange={(newOptions) =>
updateSelectedPolicyIds(newOptions.map((option: any) => option.key))
}
isClearable={true}
isLoading={isLoading}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/*
* 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 React, { useMemo } from 'react';
import type { EuiComboBoxOptionOption, EuiSuperSelectOption } from '@elastic/eui';
import { EuiIcon, EuiSpacer, EuiText, EuiToolTip } from '@elastic/eui';

import { FormattedMessage } from '@kbn/i18n-react';

import type { AgentPolicy, Output, PackageInfo } from '../../../../../../../../../common';
import {
FLEET_APM_PACKAGE,
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
SO_SEARCH_LIMIT,
} from '../../../../../../../../../common';
import { outputType } from '../../../../../../../../../common/constants';
import { isPackageLimited } from '../../../../../../../../../common/services';
import { useGetAgentPolicies, useGetOutputs, useGetPackagePolicies } from '../../../../../../hooks';

export function useAgentPoliciesOptions(packageInfo?: PackageInfo) {
// Fetch agent policies info
const {
data: agentPoliciesData,
error: agentPoliciesError,
isLoading: isAgentPoliciesLoading,
} = useGetAgentPolicies({
page: 1,
perPage: SO_SEARCH_LIMIT,
sortField: 'name',
sortOrder: 'asc',
noAgentCount: true, // agentPolicy.agents will always be 0
full: false, // package_policies will always be empty
});
const agentPolicies = useMemo(
() => agentPoliciesData?.items.filter((policy) => !policy.is_managed) || [],
[agentPoliciesData?.items]
);

const { data: outputsData, isLoading: isOutputLoading } = useGetOutputs();

// get all package policies with apm integration or the current integration
const { data: packagePoliciesForThisPackage, isLoading: isLoadingPackagePolicies } =
useGetPackagePolicies({
page: 1,
perPage: SO_SEARCH_LIMIT,
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: ${packageInfo?.name}`,
});

const packagePoliciesForThisPackageByAgentPolicyId = useMemo(
() =>
packagePoliciesForThisPackage?.items.reduce(
(acc: { [key: string]: boolean }, packagePolicy) => {
packagePolicy.policy_ids.forEach((policyId) => {
acc[policyId] = true;
});
return acc;
},
{}
),
[packagePoliciesForThisPackage?.items]
);

const { getDataOutputForPolicy } = useMemo(() => {
const defaultOutput = (outputsData?.items ?? []).find((output) => output.is_default);
const outputsById = (outputsData?.items ?? []).reduce(
(acc: { [key: string]: Output }, output) => {
acc[output.id] = output;
return acc;
},
{}
);

return {
getDataOutputForPolicy: (policy: Pick<AgentPolicy, 'data_output_id'>) => {
return policy.data_output_id ? outputsById[policy.data_output_id] : defaultOutput;
},
};
}, [outputsData]);

const agentPolicyOptions: Array<EuiSuperSelectOption<string>> = useMemo(
() =>
packageInfo
? agentPolicies.map((policy) => {
const isLimitedPackageAlreadyInPolicy =
isPackageLimited(packageInfo!) &&
packagePoliciesForThisPackageByAgentPolicyId?.[policy.id];

const isAPMPackageAndDataOutputIsLogstash =
packageInfo?.name === FLEET_APM_PACKAGE &&
getDataOutputForPolicy(policy)?.type === outputType.Logstash;

return {
inputDisplay: (
<>
<EuiText size="s">{policy.name}</EuiText>
{isAPMPackageAndDataOutputIsLogstash && (
<>
<EuiSpacer size="xs" />
<EuiText size="s">
<FormattedMessage
id="xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyDisabledAPMLogstashOuputText"
defaultMessage="Logstash output for integrations is not supported with APM"
/>
</EuiText>
</>
)}
</>
),
value: policy.id,
disabled: isLimitedPackageAlreadyInPolicy || isAPMPackageAndDataOutputIsLogstash,
'data-test-subj': 'agentPolicyItem',
};
})
: [],
[
packageInfo,
agentPolicies,
packagePoliciesForThisPackageByAgentPolicyId,
getDataOutputForPolicy,
]
);

const agentPolicyMultiOptions: Array<EuiComboBoxOptionOption<string>> = useMemo(
() =>
packageInfo && !isOutputLoading && !isAgentPoliciesLoading && !isLoadingPackagePolicies
? agentPolicies.map((policy) => {
const isLimitedPackageAlreadyInPolicy =
isPackageLimited(packageInfo!) &&
packagePoliciesForThisPackageByAgentPolicyId?.[policy.id];

const isAPMPackageAndDataOutputIsLogstash =
packageInfo?.name === FLEET_APM_PACKAGE &&
getDataOutputForPolicy(policy)?.type === outputType.Logstash;

return {
append: isAPMPackageAndDataOutputIsLogstash ? (
<EuiToolTip
content={
<FormattedMessage
id="xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyDisabledAPMLogstashOuputText"
defaultMessage="Logstash output for integrations is not supported with APM"
/>
}
>
<EuiIcon size="s" type="warningFilled" />
</EuiToolTip>
) : null,
key: policy.id,
label: policy.name,
disabled: isLimitedPackageAlreadyInPolicy || isAPMPackageAndDataOutputIsLogstash,
'data-test-subj': 'agentPolicyMultiItem',
};
})
: [],
[
packageInfo,
agentPolicies,
packagePoliciesForThisPackageByAgentPolicyId,
getDataOutputForPolicy,
isOutputLoading,
isAgentPoliciesLoading,
isLoadingPackagePolicies,
]
);

return {
agentPoliciesError,
isLoading: isOutputLoading || isAgentPoliciesLoading || isLoadingPackagePolicies,
agentPolicyOptions,
agentPolicies,
agentPolicyMultiOptions,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describe('step select agent policy', () => {
agentPolicies={[]}
updateAgentPolicies={updateAgentPoliciesMock}
setHasAgentPolicyError={mockSetHasAgentPolicyError}
selectedAgentPolicyIds={selectedAgentPolicyIds}
initialSelectedAgentPolicyIds={selectedAgentPolicyIds}
/>
));

Expand Down
Loading

0 comments on commit 813fc21

Please sign in to comment.