[Fleet] Change agent policies in edit package policy page (#186084)

## Summary

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

Added agent policy selection to Edit integration policy page.


There is a lot of duplication between Create and Edit integration policy
pages, I'll see if I can refactor to extract the common logic: steps
components and managing its state.
I extracted the steps to a hook, it would be a bigger refactor to use
this in Create package policy page, so I might create a follow up issue
for that.

## To verify
- enable the `enableReusableIntegrationPolicies` experimental feature in
`kibana.dev.yml`
- Create a few agent policies
- Add an integration 
- Go to Edit integration, and modify the linked agent policies
- Verify that the existing agent policies are populated correctly in the
Existing hosts combo box
- Verify that the modified agent policy list is reflected in the
`Preview API Request`, `policy_ids` list.
- Verify that when submitting the form, the package policy linkages are
updated to the selected ones (add/remove agent policies)
- The agent count should update below the combo / in the submit modal
window
- It's not allowed to submit the form after removing all agent policies
- If a new agent policy is selected, it will be created first and then
assigned to the integration policy

<img width="995" alt="image"
src="0a7163c6-154e-49b1-b73c-19ed024f6dc3">
<img width="993" alt="image"
src="ad470a27-90fa-40f5-b394-a93a08c95e06">
<img width="535" alt="image"
src="3b0ddc29-abf8-4e0d-8beb-300634c245b3">
<img width="1758" alt="image"
src="e8b976fe-3e53-439c-9b23-803deaf3e0aa">

### Create agent policy
<img width="1737" alt="image"
src="6f2a7f65-981a-487d-87c4-2dbb7ecd1835">
Preview API request contains the POST agent policy request
<img width="896" alt="image"
src="109140ab-13f2-42c9-9bbc-fb64859c4f62">
After submit, the updated integration policy is assigned to the new
agent policy too
<img width="2552" alt="image"
src="4027b47b-8d20-4153-b7ec-ed3500f08c9a">


## Open questions

- Currently the namespace placeholder of the package policy is set to
show the namespace of the first selected agent policy (if not set by the
package policy). I have to check what happens on the backend, if the
inherited namespace is changed if the agent policies change. The
behaviour should be consistent in the backend and UI.
Currently on the Agent policy details UI, the same integration policy
might show different inherited namespace if its shared by multiple agent
policies with different namespace.

<img width="1498" alt="image"
src="567800a8-2dcb-4b18-af89-f6e902889092">
<img width="1326" alt="image"
src="b59d131e-314c-4d5a-81e3-ab8fe0fa6e1f">
<img width="1318" alt="image"
src="69b54a63-f7c1-4f0f-8041-74b1774f1e9e">

- When the Edit integration was started from the Agent policy details
UI, the navigation goes back to the same agent policy after submitting
the page. Is this okay? Might be somewhat unexpected if creating a new
agent policy, though it is getting complex to decide where to navigate
in case of multiple agent policies.


### Checklist

- [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

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: criamico <mariacristina.amico@elastic.co>
This commit is contained in:
Julia Bardi 2024-06-20 18:26:35 +02:00 committed by GitHub
parent 592aafcaba
commit ffa943c8be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 813 additions and 195 deletions

View file

@ -0,0 +1,64 @@
/*
* 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 type { AgentPolicy } from '../types';
import { getInheritedNamespace } from './agent_policies_helpers';
describe('getInheritedNamespace', () => {
const agentPolicy: AgentPolicy[] = [
{
id: 'agent-policy-1',
namespace: 'testnamespace',
name: 'Agent policy 1',
is_managed: false,
status: 'active',
updated_at: '',
updated_by: '',
revision: 1,
package_policies: [],
is_protected: false,
},
];
const agentPolicies: AgentPolicy[] = [
{
id: 'agent-policy-1',
namespace: 'testnamespace',
name: 'Agent policy 1',
is_managed: false,
status: 'active',
updated_at: '',
updated_by: '',
revision: 1,
package_policies: [],
is_protected: false,
},
{
id: 'agent-policy-2',
namespace: 'default',
name: 'Agent policy 2',
is_managed: false,
status: 'active',
updated_at: '',
updated_by: '',
revision: 1,
package_policies: [],
is_protected: false,
},
];
it('should return the policy namespace when there is only one agent policy', () => {
expect(getInheritedNamespace(agentPolicy)).toEqual('testnamespace');
});
it('should return default namespace when there is are multiple agent policies', () => {
expect(getInheritedNamespace(agentPolicies)).toEqual('default');
});
it('should return default namespace when there are no agent policies', () => {
expect(getInheritedNamespace([])).toEqual('default');
});
});

View file

@ -44,3 +44,10 @@ function policyHasIntegration(agentPolicy: AgentPolicy, packageName: string) {
return agentPolicy.package_policies?.some((p) => p.package?.name === packageName);
}
export function getInheritedNamespace(agentPolicies: AgentPolicy[]): string {
if (agentPolicies.length === 1) {
return agentPolicies[0].namespace;
}
return 'default';
}

View file

@ -73,6 +73,7 @@ export {
policyHasAPMIntegration,
policyHasEndpointSecurity,
policyHasSyntheticsIntegration,
getInheritedNamespace,
} from './agent_policies_helpers';
export {

View file

@ -102,6 +102,7 @@ describe('Add Integration - Mock API', () => {
],
};
cy.intercept('/api/fleet/agent_policies?*', { items: [agentPolicy] });
cy.intercept('/api/fleet/agent_policies/_bulk_get', { items: [agentPolicy] });
cy.intercept('/api/fleet/agent_policies/policy-1', {
item: agentPolicy,
});

View file

@ -57,17 +57,19 @@ describe('Edit package policy', () => {
hasErrors: false,
},
]);
const agentPolicy = {
id: 'fleet-server-policy',
name: 'Fleet server policy 1',
description: '',
namespace: 'default',
monitoring_enabled: ['logs', 'metrics'],
status: 'active',
package_policies: [{ id: 'policy-1', name: 'fleet_server-1' }],
};
cy.intercept('/api/fleet/agent_policies/fleet-server-policy', {
item: {
id: 'fleet-server-policy',
name: 'Fleet server policy 1',
description: '',
namespace: 'default',
monitoring_enabled: ['logs', 'metrics'],
status: 'active',
package_policies: [{ id: 'policy-1', name: 'fleet_server-1' }],
},
item: agentPolicy,
});
cy.intercept('/api/fleet/agent_policies/_bulk_get', { items: [agentPolicy] });
cy.intercept('/api/fleet/epm/packages/fleet_server*', {
item: {
name: 'fleet_server',

View file

@ -0,0 +1,58 @@
/*
* 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 from 'react';
import type { TestRenderer } from '../../../../../mock';
import { createFleetTestRendererMock } from '../../../../../mock';
import { ConfirmDeployAgentPolicyModal } from './confirm_deploy_modal';
describe('ConfirmDeployAgentPolicyModal', () => {
let testRenderer: TestRenderer;
let renderResult: ReturnType<typeof testRenderer.render>;
const render = (props: any) =>
(renderResult = testRenderer.render(
<ConfirmDeployAgentPolicyModal
onConfirm={jest.fn()}
onCancel={jest.fn()}
agentCount={0}
agentPolicies={[{ name: 'Agent policy 1' }, { name: 'Agent policy 2' }]}
agentPoliciesToAdd={[]}
agentPoliciesToRemove={[]}
{...props}
/>
));
it('should render agent count with agent policies', () => {
testRenderer = createFleetTestRendererMock();
render({
agentCount: 1,
});
expect(renderResult.getByText('This action will update 1 agent')).toBeInTheDocument();
expect(renderResult.getByText('Agent policy 1, Agent policy 2')).toBeInTheDocument();
});
it('should render agent policies to add and remove if no agent count', () => {
testRenderer = createFleetTestRendererMock();
render({
agentCount: 0,
agentPoliciesToAdd: ['Agent policy 1', 'Agent policy 2'],
agentPoliciesToRemove: ['Agent policy 3'],
});
expect(
renderResult.getByText('This action will update the selected agent policies')
).toBeInTheDocument();
const calloutText = renderResult.getByTestId('confirmAddRemovePoliciesCallout').textContent;
expect(calloutText).toContain(
'Agent policies that will be updated to use this integration policy:Agent policy 1Agent policy 2'
);
expect(calloutText).toContain(
'Agent policies that will be updated to remove this integration policy:Agent policy 3'
);
});
});

View file

@ -17,17 +17,15 @@ export const ConfirmDeployAgentPolicyModal: React.FunctionComponent<{
onCancel: () => void;
agentCount: number;
agentPolicies: AgentPolicy[];
showUnprivilegedAgentsCallout?: boolean;
unprivilegedAgentsCount?: number;
dataStreams?: Array<{ name: string; title: string }>;
agentPoliciesToAdd?: string[];
agentPoliciesToRemove?: string[];
}> = ({
onConfirm,
onCancel,
agentCount,
agentPolicies,
showUnprivilegedAgentsCallout = false,
unprivilegedAgentsCount = 0,
dataStreams,
agentPoliciesToAdd = [],
agentPoliciesToRemove = [],
}) => {
return (
<EuiConfirmModal
@ -53,28 +51,68 @@ export const ConfirmDeployAgentPolicyModal: React.FunctionComponent<{
}
buttonColor="primary"
>
<EuiCallOut
iconType="iInCircle"
title={i18n.translate('xpack.fleet.agentPolicy.confirmModalCalloutTitle', {
defaultMessage:
'This action will update {agentCount, plural, one {# agent} other {# agents}}',
values: {
agentCount,
},
})}
>
<div className="eui-textBreakWord">
<FormattedMessage
id="xpack.fleet.agentPolicy.confirmModalCalloutDescription"
defaultMessage="Fleet has detected that the selected agent policies, {policyNames}, are already in use by
{agentCount > 0 ? (
<EuiCallOut
iconType="iInCircle"
title={i18n.translate('xpack.fleet.agentPolicy.confirmModalCalloutTitle', {
defaultMessage:
'This action will update {agentCount, plural, one {# agent} other {# agents}}',
values: {
agentCount,
},
})}
>
<div className="eui-textBreakWord">
<FormattedMessage
id="xpack.fleet.agentPolicy.confirmModalCalloutDescription"
defaultMessage="Fleet has detected that the selected agent policies, {policyNames}, are already in use by
some of your agents. As a result of this action, Fleet will deploy updates to all agents
that use these policies."
values={{
policyNames: <b>{agentPolicies.map((policy) => policy.name).join(', ')}</b>,
}}
/>
</div>
</EuiCallOut>
values={{
policyNames: <b>{agentPolicies.map((policy) => policy.name).join(', ')}</b>,
}}
/>
</div>
</EuiCallOut>
) : (
<EuiCallOut
data-test-subj="confirmAddRemovePoliciesCallout"
iconType="iInCircle"
title={i18n.translate('xpack.fleet.agentPolicy.confirmModalPoliciesCalloutTitle', {
defaultMessage: 'This action will update the selected agent policies',
values: {
agentCount,
},
})}
>
{agentPoliciesToAdd.length > 0 && (
<div className="eui-textBreakWord">
<FormattedMessage
id="xpack.fleet.agentPolicy.confirmModalPoliciesAddCalloutDescription"
defaultMessage="Agent policies that will be updated to use this integration policy:"
/>
<ul>
{agentPoliciesToAdd.map((policy) => (
<li key={policy}>{policy}</li>
))}
</ul>
</div>
)}
{agentPoliciesToRemove.length > 0 && (
<div className="eui-textBreakWord">
<FormattedMessage
id="xpack.fleet.agentPolicy.confirmModalPoliciesRemoveCalloutDescription"
defaultMessage="Agent policies that will be updated to remove this integration policy:"
/>
<ul>
{agentPoliciesToRemove.map((policy) => (
<li key={policy}>{policy}</li>
))}
</ul>
</div>
)}
</EuiCallOut>
)}
<EuiSpacer size="l" />
<FormattedMessage
id="xpack.fleet.agentPolicy.confirmModalDescription"

View file

@ -8,6 +8,8 @@
import React from 'react';
import { act, fireEvent, waitFor } from '@testing-library/react';
import { getInheritedNamespace } from '../../../../../../../../common/services';
import type { TestRenderer } from '../../../../../../../mock';
import { createFleetTestRendererMock } from '../../../../../../../mock';
import type { AgentPolicy, NewPackagePolicy, PackageInfo } from '../../../../../types';
@ -92,7 +94,7 @@ describe('StepDefinePackagePolicy', () => {
const render = () =>
(renderResult = testRenderer.render(
<StepDefinePackagePolicy
agentPolicies={agentPolicies}
namespacePlaceholder={getInheritedNamespace(agentPolicies)}
packageInfo={packageInfo}
packagePolicy={packagePolicy}
updatePackagePolicy={mockUpdatePackagePolicy}

View file

@ -24,12 +24,7 @@ import {
import styled from 'styled-components';
import type {
AgentPolicy,
PackageInfo,
NewPackagePolicy,
RegistryVarsEntry,
} from '../../../../../types';
import type { PackageInfo, NewPackagePolicy, RegistryVarsEntry } from '../../../../../types';
import { Loading } from '../../../../../components';
import { useStartServices } from '../../../../../hooks';
@ -48,7 +43,7 @@ const FormGroupResponsiveFields = styled(EuiDescribedFormGroup)`
`;
export const StepDefinePackagePolicy: React.FunctionComponent<{
agentPolicies?: AgentPolicy[];
namespacePlaceholder?: string;
packageInfo: PackageInfo;
packagePolicy: NewPackagePolicy;
updatePackagePolicy: (fields: Partial<NewPackagePolicy>) => void;
@ -58,7 +53,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{
noAdvancedToggle?: boolean;
}> = memo(
({
agentPolicies,
namespacePlaceholder,
packageInfo,
packagePolicy,
updatePackagePolicy,
@ -290,7 +285,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{
<EuiComboBox
data-test-subj="packagePolicyNamespaceInput"
noSuggestions
placeholder={agentPolicies?.[0]?.namespace}
placeholder={namespacePlaceholder}
isDisabled={isEditPage && packageInfo.type === 'input'}
singleSelection={true}
selectedOptions={

View file

@ -67,14 +67,14 @@ describe('step select agent policy', () => {
let renderResult: ReturnType<typeof testRenderer.render>;
const mockSetHasAgentPolicyError = jest.fn();
const updateAgentPoliciesMock = jest.fn();
const render = (packageInfo?: PackageInfo, selectedAgentPolicyId?: string) =>
const render = (packageInfo?: PackageInfo, selectedAgentPolicyIds: string[] = []) =>
(renderResult = testRenderer.render(
<StepSelectAgentPolicy
packageInfo={packageInfo || ({ name: 'apache' } as any)}
agentPolicies={[]}
updateAgentPolicies={updateAgentPoliciesMock}
setHasAgentPolicyError={mockSetHasAgentPolicyError}
selectedAgentPolicyId={selectedAgentPolicyId}
selectedAgentPolicyIds={selectedAgentPolicyIds}
/>
));
@ -182,7 +182,7 @@ describe('step select agent policy', () => {
});
test('should select agent policy if pre selected', async () => {
render(undefined, 'policy-1');
render(undefined, ['policy-1']);
await act(async () => {}); // Needed as updateAgentPolicies is called after multiple useEffect
await act(async () => {
expect(updateAgentPoliciesMock).toBeCalledWith([{ id: 'policy-1', package_policies: [] }]);

View file

@ -217,13 +217,13 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{
agentPolicies: AgentPolicy[];
updateAgentPolicies: (agentPolicies: AgentPolicy[]) => void;
setHasAgentPolicyError: (hasError: boolean) => void;
selectedAgentPolicyId?: string;
selectedAgentPolicyIds: string[];
}> = ({
packageInfo,
agentPolicies,
updateAgentPolicies: updateSelectedAgentPolicies,
setHasAgentPolicyError,
selectedAgentPolicyId,
selectedAgentPolicyIds,
}) => {
const { isReady: isFleetReady } = useFleetStatus();
@ -240,8 +240,10 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{
} = useAgentPoliciesOptions(packageInfo);
const [selectedPolicyIds, setSelectedPolicyIds] = useState<string[]>([]);
const [isFirstLoad, setIsFirstLoad] = useState<boolean>(true);
const [isLoadingSelectedAgentPolicy, setIsLoadingSelectedAgentPolicy] = useState<boolean>(false);
const [isLoadingSelectedAgentPolicies, setIsLoadingSelectedAgentPolicies] =
useState<boolean>(false);
const [selectedAgentPolicies, setSelectedAgentPolicies] = useState<AgentPolicy[]>(agentPolicies);
const updateAgentPolicies = useCallback(
@ -255,7 +257,7 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{
useEffect(() => {
const fetchAgentPolicyInfo = async () => {
if (selectedPolicyIds.length > 0) {
setIsLoadingSelectedAgentPolicy(true);
setIsLoadingSelectedAgentPolicies(true);
const { data, error } = await sendBulkGetAgentPolicies(selectedPolicyIds, { full: true });
if (error) {
setSelectedAgentPolicyError(error);
@ -264,7 +266,7 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{
setSelectedAgentPolicyError(undefined);
updateAgentPolicies(data.items);
}
setIsLoadingSelectedAgentPolicy(false);
setIsLoadingSelectedAgentPolicies(false);
} else {
setSelectedAgentPolicyError(undefined);
updateAgentPolicies([]);
@ -284,25 +286,27 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{
// Try to select default agent policy
useEffect(() => {
if (
isFirstLoad &&
selectedPolicyIds.length === 0 &&
existingAgentPolicies.length &&
(enableReusableIntegrationPolicies
? agentPolicyMultiOptions.length
: agentPolicyOptions.length)
) {
setIsFirstLoad(false);
if (enableReusableIntegrationPolicies) {
const enabledOptions = agentPolicyMultiOptions.filter((option) => !option.disabled);
if (enabledOptions.length === 1) {
setSelectedPolicyIds([enabledOptions[0].key!]);
} else if (selectedAgentPolicyId) {
setSelectedPolicyIds([selectedAgentPolicyId]);
} else if (selectedAgentPolicyIds.length > 0) {
setSelectedPolicyIds(selectedAgentPolicyIds);
}
} else {
const enabledOptions = agentPolicyOptions.filter((option) => !option.disabled);
if (enabledOptions.length === 1) {
setSelectedPolicyIds([enabledOptions[0].value]);
} else if (selectedAgentPolicyId) {
setSelectedPolicyIds([selectedAgentPolicyId]);
} else if (selectedAgentPolicyIds.length > 0) {
setSelectedPolicyIds(selectedAgentPolicyIds);
}
}
}
@ -310,9 +314,10 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{
agentPolicyOptions,
agentPolicyMultiOptions,
enableReusableIntegrationPolicies,
selectedAgentPolicyId,
selectedAgentPolicyIds,
selectedPolicyIds,
existingAgentPolicies,
isFirstLoad,
]);
// Bubble up any issues with agent policy selection
@ -342,6 +347,14 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{
);
}
const someNewAgentPoliciesHaveLimitedPackage =
!packageInfo ||
selectedAgentPolicies
.filter((policy) => !selectedAgentPolicyIds.find((id) => policy.id === id))
.some((selectedAgentPolicy) =>
doesAgentPolicyHaveLimitedPackage(selectedAgentPolicy, packageInfo)
);
return (
<>
<EuiFlexGroup direction="column" gutterSize="m">
@ -381,7 +394,7 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{
</EuiFlexGroup>
}
helpText={
isFleetReady && selectedPolicyIds.length > 0 && !isLoadingSelectedAgentPolicy ? (
isFleetReady && selectedPolicyIds.length > 0 && !isLoadingSelectedAgentPolicies ? (
<FormattedMessage
id="xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyAgentsDescriptionText"
defaultMessage="{count, plural, one {# agent is} other {# agents are}} enrolled with the selected agent policies."
@ -395,11 +408,7 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{
) : null
}
isInvalid={Boolean(
selectedPolicyIds.length === 0 ||
!packageInfo ||
selectedAgentPolicies.every((selectedAgentPolicy) =>
doesAgentPolicyHaveLimitedPackage(selectedAgentPolicy, packageInfo)
)
selectedPolicyIds.length === 0 || someNewAgentPoliciesHaveLimitedPackage
)}
error={
selectedPolicyIds.length === 0 ? (
@ -407,17 +416,17 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{
id="xpack.fleet.createPackagePolicy.StepSelectPolicy.noPolicySelectedError"
defaultMessage="An agent policy is required."
/>
) : (
) : someNewAgentPoliciesHaveLimitedPackage ? (
<FormattedMessage
id="xpack.fleet.createPackagePolicy.StepSelectPolicy.cannotAddLimitedIntegrationError"
defaultMessage="This integration can only be added once per agent policy."
/>
)
) : null
}
>
{enableReusableIntegrationPolicies ? (
<AgentPolicyMultiSelect
isLoading={isLoading || !packageInfo || isLoadingSelectedAgentPolicy}
isLoading={isLoading || !packageInfo || isLoadingSelectedAgentPolicies}
selectedPolicyIds={selectedPolicyIds}
setSelectedPolicyIds={setSelectedPolicyIds}
agentPolicyMultiOptions={agentPolicyMultiOptions}
@ -431,7 +440,7 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{
}
)}
fullWidth
isLoading={isLoading || !packageInfo || isLoadingSelectedAgentPolicy}
isLoading={isLoading || !packageInfo || isLoadingSelectedAgentPolicies}
options={agentPolicyOptions}
valueOfSelected={selectedPolicyIds[0]}
onChange={onChange}

View file

@ -93,7 +93,7 @@ describe('StepSelectHosts', () => {
packageInfo={packageInfo}
setHasAgentPolicyError={jest.fn()}
updateSelectedTab={jest.fn()}
selectedAgentPolicyId={undefined}
selectedAgentPolicyIds={[]}
/>
));
beforeEach(() => {

View file

@ -42,7 +42,8 @@ interface Props {
packageInfo?: PackageInfo;
setHasAgentPolicyError: (hasError: boolean) => void;
updateSelectedTab: (tab: SelectedPolicyTab) => void;
selectedAgentPolicyId?: string;
selectedAgentPolicyIds: string[];
initialSelectedTabIndex?: number;
}
export const StepSelectHosts: React.FunctionComponent<Props> = ({
@ -56,7 +57,8 @@ export const StepSelectHosts: React.FunctionComponent<Props> = ({
packageInfo,
setHasAgentPolicyError,
updateSelectedTab,
selectedAgentPolicyId,
selectedAgentPolicyIds,
initialSelectedTabIndex,
}) => {
let existingAgentPolicies: AgentPolicy[] = [];
const { data: agentPoliciesData, error: err } = useGetAgentPolicies({
@ -110,7 +112,7 @@ export const StepSelectHosts: React.FunctionComponent<Props> = ({
agentPolicies={agentPolicies}
updateAgentPolicies={updateAgentPolicies}
setHasAgentPolicyError={setHasAgentPolicyError}
selectedAgentPolicyId={selectedAgentPolicyId}
selectedAgentPolicyIds={selectedAgentPolicyIds}
/>
),
},
@ -121,7 +123,13 @@ export const StepSelectHosts: React.FunctionComponent<Props> = ({
return existingAgentPolicies.length > 0 ? (
<StyledEuiTabbedContent
initialSelectedTab={selectedAgentPolicyId ? tabs[1] : tabs[0]}
initialSelectedTab={
initialSelectedTabIndex
? tabs[initialSelectedTabIndex]
: selectedAgentPolicyIds.length > 0
? tabs[1]
: tabs[0]
}
tabs={tabs}
onTabClick={handleOnTabClick}
/>

View file

@ -8,10 +8,14 @@
import { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { omit } from 'lodash';
import { set } from '@kbn/safer-lodash-set';
import { ExperimentalFeaturesService } from '../../../../../services';
import {
generateCreatePackagePolicyDevToolsRequest,
generateCreateAgentPolicyDevToolsRequest,
generateUpdatePackagePolicyDevToolsRequest,
} from '../../../services';
import {
FLEET_SYSTEM_PACKAGE,
@ -26,12 +30,14 @@ export function useDevToolsRequest({
packageInfo,
selectedPolicyTab,
withSysMonitoring,
packagePolicyId,
}: {
withSysMonitoring: boolean;
selectedPolicyTab: SelectedPolicyTab;
newAgentPolicy: NewAgentPolicy;
packagePolicy: NewPackagePolicy;
packageInfo?: PackageInfo;
packagePolicyId?: string;
}) {
const { showDevtoolsRequest: isShowDevtoolRequestExperimentEnabled } =
ExperimentalFeaturesService.get();
@ -47,28 +53,55 @@ export function useDevToolsRequest({
`${generateCreateAgentPolicyDevToolsRequest(
newAgentPolicy,
withSysMonitoring && !packagePolicyIsSystem
)}\n\n${generateCreatePackagePolicyDevToolsRequest({
...{ ...packagePolicy, policy_id: '' },
})}`,
i18n.translate(
'xpack.fleet.createPackagePolicy.devtoolsRequestWithAgentPolicyDescription',
{
defaultMessage:
'These Kibana requests create a new agent policy and a new package policy.',
}
),
)}\n\n${
packagePolicyId
? generateUpdatePackagePolicyDevToolsRequest(
packagePolicyId,
set(omit(packagePolicy, 'elasticsearch', 'policy_id'), 'policy_ids', [
...packagePolicy.policy_ids,
'',
])
)
: generateCreatePackagePolicyDevToolsRequest({
...{ ...packagePolicy, policy_ids: [''] },
})
}`,
packagePolicyId
? i18n.translate(
'xpack.fleet.editPackagePolicy.devtoolsRequestWithAgentPolicyDescription',
{
defaultMessage:
'These Kibana requests create a new agent policy and update a package policy.',
}
)
: i18n.translate(
'xpack.fleet.createPackagePolicy.devtoolsRequestWithAgentPolicyDescription',
{
defaultMessage:
'These Kibana requests create a new agent policy and a new package policy.',
}
),
];
}
return [
generateCreatePackagePolicyDevToolsRequest({
...packagePolicy,
}),
i18n.translate('xpack.fleet.createPackagePolicy.devtoolsRequestDescription', {
defaultMessage: 'This Kibana request creates a new package policy.',
}),
packagePolicyId
? generateUpdatePackagePolicyDevToolsRequest(
packagePolicyId,
omit(packagePolicy, 'elasticsearch', 'policy_id')
)
: generateCreatePackagePolicyDevToolsRequest({
...packagePolicy,
}),
packagePolicyId
? i18n.translate('xpack.fleet.editPackagePolicy.devtoolsRequestDescription', {
defaultMessage: 'This Kibana request updates package policy.',
})
: i18n.translate('xpack.fleet.createPackagePolicy.devtoolsRequestDescription', {
defaultMessage: 'This Kibana request creates a new package policy.',
}),
];
}, [packagePolicy, newAgentPolicy, withSysMonitoring, selectedPolicyTab]);
}, [packagePolicy, newAgentPolicy, withSysMonitoring, selectedPolicyTab, packagePolicyId]);
return { showDevtoolsRequest, devtoolRequest, devtoolRequestDescription };
}

View file

@ -49,7 +49,7 @@ import {
import { useAgentless } from './setup_technology';
async function createAgentPolicy({
export async function createAgentPolicy({
packagePolicy,
newAgentPolicy,
withSysMonitoring,
@ -72,6 +72,45 @@ async function createAgentPolicy({
return resp.data.item;
}
export const createAgentPolicyIfNeeded = async ({
selectedPolicyTab,
withSysMonitoring,
newAgentPolicy,
packagePolicy,
packageInfo,
}: {
selectedPolicyTab: SelectedPolicyTab;
withSysMonitoring: boolean;
newAgentPolicy: NewAgentPolicy;
packagePolicy: NewPackagePolicy;
packageInfo?: PackageInfo;
}): Promise<AgentPolicy | undefined> => {
if (selectedPolicyTab === SelectedPolicyTab.NEW) {
if ((withSysMonitoring || newAgentPolicy.monitoring_enabled?.length) ?? 0 > 0) {
const packagesToPreinstall: Array<string | { name: string; version: string }> = [];
// skip preinstall of input package, to be able to rollback when package policy creation fails
if (packageInfo && packageInfo.type !== 'input') {
packagesToPreinstall.push({ name: packageInfo.name, version: packageInfo.version });
}
if (withSysMonitoring) {
packagesToPreinstall.push(FLEET_SYSTEM_PACKAGE);
}
if (newAgentPolicy.monitoring_enabled?.length ?? 0 > 0) {
packagesToPreinstall.push(FLEET_ELASTIC_AGENT_PACKAGE);
}
if (packagesToPreinstall.length > 0) {
await sendBulkInstallPackages([...new Set(packagesToPreinstall)]);
}
}
return await createAgentPolicy({
newAgentPolicy,
packagePolicy,
withSysMonitoring,
});
}
};
async function savePackagePolicy(pkgPolicy: CreatePackagePolicyRequest['body']) {
const { policy, forceCreateNeeded } = await prepareInputPackagePolicyDataset(pkgPolicy);
const result = await sendCreatePackagePolicy({
@ -281,33 +320,21 @@ export function useOnSubmit({
return;
}
let createdPolicy = overrideCreatedAgentPolicy;
if (selectedPolicyTab === SelectedPolicyTab.NEW && !overrideCreatedAgentPolicy) {
if (!overrideCreatedAgentPolicy) {
try {
setFormState('LOADING');
if ((withSysMonitoring || newAgentPolicy.monitoring_enabled?.length) ?? 0 > 0) {
const packagesToPreinstall: Array<string | { name: string; version: string }> = [];
// skip preinstall of input package, to be able to rollback when package policy creation fails
if (packageInfo && packageInfo.type !== 'input') {
packagesToPreinstall.push({ name: packageInfo.name, version: packageInfo.version });
}
if (withSysMonitoring) {
packagesToPreinstall.push(FLEET_SYSTEM_PACKAGE);
}
if (newAgentPolicy.monitoring_enabled?.length ?? 0 > 0) {
packagesToPreinstall.push(FLEET_ELASTIC_AGENT_PACKAGE);
}
if (packagesToPreinstall.length > 0) {
await sendBulkInstallPackages([...new Set(packagesToPreinstall)]);
}
}
createdPolicy = await createAgentPolicy({
const newPolicy = await createAgentPolicyIfNeeded({
newAgentPolicy,
packagePolicy,
withSysMonitoring,
packageInfo,
selectedPolicyTab,
});
setAgentPolicies([createdPolicy]);
updatePackagePolicy({ policy_ids: [createdPolicy.id] });
if (newPolicy) {
createdPolicy = newPolicy;
setAgentPolicies([createdPolicy]);
updatePackagePolicy({ policy_ids: [createdPolicy.id] });
}
} catch (e) {
setFormState('VALID');
notifications.toasts.addError(e, {

View file

@ -35,6 +35,7 @@ import {
import { useCancelAddPackagePolicy } from '../hooks';
import {
getInheritedNamespace,
getRootPrivilegedDataStreams,
isRootPrivilegesRequired,
splitPkgKey,
@ -81,7 +82,7 @@ import { PostInstallGoogleCloudShellModal } from './components/cloud_security_po
import { PostInstallAzureArmTemplateModal } from './components/cloud_security_posture/post_install_azure_arm_template_modal';
import { RootPrivilegesCallout } from './root_callout';
const StepsWithLessPadding = styled(EuiSteps)`
export const StepsWithLessPadding = styled(EuiSteps)`
.euiStep__content {
padding-bottom: ${(props) => props.theme.eui.euiSizeM};
}
@ -293,7 +294,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
packageInfo={packageInfo}
setHasAgentPolicyError={setHasAgentPolicyError}
updateSelectedTab={updateSelectedPolicyTab}
selectedAgentPolicyId={queryParamsPolicyId}
selectedAgentPolicyIds={queryParamsPolicyId ? [queryParamsPolicyId] : []}
/>
),
[
@ -377,7 +378,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
) : packageInfo ? (
<>
<StepDefinePackagePolicy
agentPolicies={agentPolicies}
namespacePlaceholder={getInheritedNamespace(agentPolicies)}
packageInfo={packageInfo}
packagePolicy={packagePolicy}
updatePackagePolicy={updatePackagePolicy}
@ -462,10 +463,6 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
}
const rootPrivilegedDataStreams = packageInfo ? getRootPrivilegedDataStreams(packageInfo) : [];
const unprivilegedAgentsCount = agentPolicies.reduce(
(acc, curr) => acc + (curr.unprivileged_agents ?? 0),
0
);
return (
<CreatePackagePolicySinglePageLayout {...layoutProps} data-test-subj="createPackagePolicy">
@ -478,13 +475,6 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
agentPolicies={agentPolicies}
onConfirm={onSubmit}
onCancel={() => setFormState('VALID')}
showUnprivilegedAgentsCallout={Boolean(
packageInfo &&
isRootPrivilegesRequired(packageInfo) &&
unprivilegedAgentsCount > 0
)}
unprivilegedAgentsCount={unprivilegedAgentsCount}
dataStreams={rootPrivilegedDataStreams}
/>
)}
{formState === 'SUBMITTED_NO_AGENTS' &&

View file

@ -7,3 +7,4 @@
export { useHistoryBlock } from './use_history_block';
export { usePackagePolicyWithRelatedData } from './use_package_policy';
export { usePackagePolicySteps } from './use_package_policy_steps';

View file

@ -15,7 +15,7 @@ import type {
UpgradePackagePolicyDryRunResponse,
} from '../../../../../../../common/types/rest_spec';
import {
sendGetOneAgentPolicy,
sendBulkGetAgentPolicies,
sendGetOnePackagePolicy,
sendGetPackageInfoByKey,
sendGetSettings,
@ -94,11 +94,14 @@ export function usePackagePolicyWithRelatedData(
const [validationResults, setValidationResults] = useState<PackagePolicyValidationResults>();
const hasErrors = validationResults ? validationHasErrors(validationResults) : false;
const savePackagePolicy = async () => {
const savePackagePolicy = async (packagePolicyOverride?: Partial<PackagePolicy>) => {
setFormState('LOADING');
const {
policy: { elasticsearch, ...restPackagePolicy },
} = await prepareInputPackagePolicyDataset(packagePolicy);
} = await prepareInputPackagePolicyDataset({
...packagePolicy,
...(packagePolicyOverride ?? {}),
});
const result = await sendUpdatePackagePolicy(packagePolicyId, restPackagePolicy);
setFormState('SUBMITTED');
@ -171,21 +174,15 @@ export function usePackagePolicyWithRelatedData(
throw packagePolicyError;
}
const newAgentPolicies = [];
for (const policyId of packagePolicyData!.item.policy_ids) {
const { data: agentPolicyData, error: agentPolicyError } = await sendGetOneAgentPolicy(
policyId
);
const { data, error: agentPolicyError } = await sendBulkGetAgentPolicies(
packagePolicyData!.item.policy_ids
);
if (agentPolicyError) {
throw agentPolicyError;
}
if (agentPolicyData?.item) {
newAgentPolicies.push(agentPolicyData.item);
}
if (agentPolicyError) {
throw agentPolicyError;
}
setAgentPolicies(newAgentPolicies);
setAgentPolicies(data?.items ?? []);
const { data: upgradePackagePolicyDryRunData, error: upgradePackagePolicyDryRunError } =
await sendUpgradePackagePolicyDryRun([packagePolicyId]);

View file

@ -0,0 +1,225 @@
/*
* 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, { useCallback, useMemo, useState, useEffect } from 'react';
import { isEqual } from 'lodash';
import { i18n } from '@kbn/i18n';
import type { EuiStepProps } from '@elastic/eui';
import type { AgentPolicy, NewAgentPolicy, NewPackagePolicy } from '../../../../../../../common';
import { generateNewAgentPolicyWithDefaults } from '../../../../../../../common/services';
import { SelectedPolicyTab, StepSelectHosts } from '../../create_package_policy_page/components';
import type { PackageInfo } from '../../../../types';
import { SetupTechnology } from '../../../../types';
import {
useDevToolsRequest,
useSetupTechnology,
} from '../../create_package_policy_page/single_page_layout/hooks';
import { agentPolicyFormValidation } from '../../components';
import { createAgentPolicyIfNeeded } from '../../create_package_policy_page/single_page_layout/hooks/form';
interface Params {
configureStep: React.ReactNode;
packageInfo?: PackageInfo;
existingAgentPolicies: AgentPolicy[];
setHasAgentPolicyError: (hasError: boolean) => void;
updatePackagePolicy: (data: { policy_ids: string[] }) => void;
agentPolicies: AgentPolicy[];
setAgentPolicies: (agentPolicies: AgentPolicy[]) => void;
isLoadingData: boolean;
packagePolicy: NewPackagePolicy;
packagePolicyId: string;
setNewAgentPolicyName: (newAgentPolicyName: string | undefined) => void;
}
export function usePackagePolicySteps({
configureStep,
packageInfo,
existingAgentPolicies,
setHasAgentPolicyError,
updatePackagePolicy,
agentPolicies,
setAgentPolicies,
isLoadingData,
packagePolicy,
packagePolicyId,
setNewAgentPolicyName,
}: Params) {
const [newAgentPolicy, setNewAgentPolicy] = useState<NewAgentPolicy>(
generateNewAgentPolicyWithDefaults({ name: 'Agent policy 1' })
);
const [withSysMonitoring, setWithSysMonitoring] = useState<boolean>(true);
const [selectedPolicyTab, setSelectedPolicyTab] = useState<SelectedPolicyTab>(
SelectedPolicyTab.EXISTING
);
const validation = agentPolicyFormValidation(newAgentPolicy);
const setPolicyValidation = useCallback(
(currentTab: SelectedPolicyTab, updatedAgentPolicy: NewAgentPolicy) => {
if (currentTab === SelectedPolicyTab.NEW) {
if (
!updatedAgentPolicy.name ||
updatedAgentPolicy.name.trim() === '' ||
!updatedAgentPolicy.namespace ||
updatedAgentPolicy.namespace.trim() === ''
) {
setHasAgentPolicyError(true);
} else {
setHasAgentPolicyError(false);
}
}
},
[setHasAgentPolicyError]
);
const updateSelectedPolicyTab = useCallback(
(currentTab) => {
setSelectedPolicyTab(currentTab);
setPolicyValidation(currentTab, newAgentPolicy);
},
[setSelectedPolicyTab, setPolicyValidation, newAgentPolicy]
);
// Update agent policy method
const updateAgentPolicies = useCallback(
(updatedAgentPolicies: AgentPolicy[]) => {
if (!isLoadingData && isEqual(updatedAgentPolicies, agentPolicies)) {
return;
}
if (updatedAgentPolicies.length > 0) {
setAgentPolicies(updatedAgentPolicies);
updatePackagePolicy({
policy_ids: updatedAgentPolicies.map((policy) => policy.id),
});
if (packageInfo) {
setHasAgentPolicyError(false);
}
} else {
setHasAgentPolicyError(true);
setAgentPolicies([]);
updatePackagePolicy({
policy_ids: [],
});
}
// eslint-disable-next-line no-console
console.debug('Agent policy updated', updatedAgentPolicies);
},
[
packageInfo,
agentPolicies,
isLoadingData,
updatePackagePolicy,
setHasAgentPolicyError,
setAgentPolicies,
]
);
const updateNewAgentPolicy = useCallback(
(updatedFields: Partial<NewAgentPolicy>) => {
const updatedAgentPolicy = {
...newAgentPolicy,
...updatedFields,
};
setNewAgentPolicy(updatedAgentPolicy);
setPolicyValidation(selectedPolicyTab, updatedAgentPolicy);
},
[setNewAgentPolicy, setPolicyValidation, newAgentPolicy, selectedPolicyTab]
);
const { selectedSetupTechnology } = useSetupTechnology({
newAgentPolicy,
updateNewAgentPolicy,
updateAgentPolicies,
setSelectedPolicyTab,
packageInfo,
});
const stepSelectAgentPolicy = useMemo(
() => (
<StepSelectHosts
agentPolicies={agentPolicies}
updateAgentPolicies={updateAgentPolicies}
newAgentPolicy={newAgentPolicy}
updateNewAgentPolicy={updateNewAgentPolicy}
withSysMonitoring={withSysMonitoring}
updateSysMonitoring={(newValue) => setWithSysMonitoring(newValue)}
validation={validation}
packageInfo={packageInfo}
setHasAgentPolicyError={setHasAgentPolicyError}
updateSelectedTab={updateSelectedPolicyTab}
selectedAgentPolicyIds={existingAgentPolicies.map((policy) => policy.id)}
initialSelectedTabIndex={1}
/>
),
[
packageInfo,
agentPolicies,
updateAgentPolicies,
newAgentPolicy,
updateNewAgentPolicy,
validation,
withSysMonitoring,
updateSelectedPolicyTab,
setHasAgentPolicyError,
existingAgentPolicies,
setWithSysMonitoring,
]
);
const steps: EuiStepProps[] = [
{
title: i18n.translate('xpack.fleet.createPackagePolicy.stepConfigurePackagePolicyTitle', {
defaultMessage: 'Configure integration',
}),
'data-test-subj': 'dataCollectionSetupStep',
children: configureStep,
headingElement: 'h2',
},
];
if (selectedSetupTechnology !== SetupTechnology.AGENTLESS) {
steps.push({
title: i18n.translate('xpack.fleet.createPackagePolicy.stepSelectAgentPolicyTitle', {
defaultMessage: 'Where to add this integration?',
}),
children: stepSelectAgentPolicy,
headingElement: 'h2',
});
}
const devToolsProps = useDevToolsRequest({
newAgentPolicy,
packagePolicy,
selectedPolicyTab,
withSysMonitoring,
packageInfo,
packagePolicyId,
});
useEffect(() => {
setNewAgentPolicyName(
selectedPolicyTab === SelectedPolicyTab.NEW ? newAgentPolicy.name : undefined
);
}, [newAgentPolicy, selectedPolicyTab, setNewAgentPolicyName]);
return {
steps,
devToolsProps,
createAgentPolicyIfNeeded: async () => {
const createdAgentPolicy = await createAgentPolicyIfNeeded({
newAgentPolicy,
packagePolicy,
withSysMonitoring,
packageInfo,
selectedPolicyTab,
});
return createdAgentPolicy?.id;
},
};
}

View file

@ -8,6 +8,8 @@
import React from 'react';
import { fireEvent, act, waitFor } from '@testing-library/react';
import { ExperimentalFeaturesService } from '../../../../../services';
import type { TestRenderer } from '../../../../../mock';
import { createFleetTestRendererMock } from '../../../../../mock';
@ -19,6 +21,9 @@ import {
sendUpgradePackagePolicyDryRun,
sendUpdatePackagePolicy,
useStartServices,
sendCreateAgentPolicy,
sendBulkGetAgentPolicies,
useGetAgentPolicies,
} from '../../../hooks';
import { useGetOnePackagePolicy } from '../../../../integrations/hooks';
@ -29,7 +34,7 @@ type MockFn = jest.MockedFunction<any>;
jest.mock('../../../hooks', () => {
return {
...jest.requireActual('../../../hooks'),
sendGetAgentStatus: jest.fn().mockResolvedValue({ data: { results: { total: 0 } } }),
sendGetAgentStatus: jest.fn(),
sendUpdatePackagePolicy: jest.fn(),
sendGetOnePackagePolicy: jest.fn(),
sendGetOneAgentPolicy: jest.fn(),
@ -125,6 +130,10 @@ jest.mock('../../../hooks', () => {
},
}),
useLink: jest.fn().mockReturnValue({ getHref: jest.fn().mockReturnValue('/navigate/path') }),
useGetAgentPolicies: jest.fn(),
sendCreateAgentPolicy: jest.fn(),
sendBulkGetAgentPolicies: jest.fn(),
sendBulkInstallPackages: jest.fn(),
};
});
@ -141,6 +150,7 @@ jest.mock('react-router-dom', () => ({
useRouteMatch: jest.fn().mockReturnValue({
params: {
packagePolicyId: 'nginx-1',
policyId: 'agent-policy-1',
},
}),
}));
@ -222,6 +232,24 @@ describe('edit package policy page', () => {
(sendUpdatePackagePolicy as MockFn).mockResolvedValue({});
(useStartServices().application.navigateToUrl as MockFn).mockReset();
(useStartServices().notifications.toasts.addError as MockFn).mockReset();
(sendGetAgentStatus as MockFn).mockResolvedValue({ data: { results: { total: 0 } } });
(sendBulkGetAgentPolicies as MockFn).mockResolvedValue({
data: { items: [{ id: 'agent-policy-1', name: 'Agent policy 1' }] },
});
(sendCreateAgentPolicy as MockFn).mockResolvedValue({
data: { item: { id: 'agent-policy-2' } },
});
(useGetAgentPolicies as MockFn).mockReturnValue({
data: {
items: [
{ id: 'agent-policy-1', name: 'Agent policy 1' },
{ id: 'fleet-server-policy', name: 'Fleet Server Policy' },
],
},
error: undefined,
isLoading: false,
resendRequest: jest.fn(),
});
});
it('should disable submit button on invalid form with empty package var', async () => {
@ -455,4 +483,57 @@ describe('edit package policy page', () => {
expect(sendUpdatePackagePolicy).toHaveBeenCalled();
});
describe('modify agent policies', () => {
beforeEach(() => {
jest
.spyOn(ExperimentalFeaturesService, 'get')
.mockReturnValue({ enableReusableIntegrationPolicies: true });
(sendGetAgentStatus as jest.MockedFunction<any>).mockResolvedValue({
data: { results: { total: 0 } },
});
});
it('should create agent policy with sys monitoring when new hosts is selected', async () => {
await act(async () => {
render();
});
await waitFor(() => {
expect(renderResult.getByTestId('agentPolicyMultiItem')).toHaveAttribute(
'title',
'Agent policy 1'
);
});
await act(async () => {
fireEvent.click(renderResult.getByTestId('newHostsTab'));
});
await act(async () => {
fireEvent.click(renderResult.getByText(/Save integration/).closest('button')!);
});
await act(async () => {
fireEvent.click(renderResult.getAllByText(/Save and deploy changes/)[1].closest('button')!);
});
expect(sendCreateAgentPolicy as jest.MockedFunction<any>).toHaveBeenCalledWith(
{
description: '',
monitoring_enabled: ['logs', 'metrics'],
name: 'Agent policy 2',
namespace: 'default',
inactivity_timeout: 1209600,
is_protected: false,
},
{ withSysMonitoring: true }
);
expect(sendUpdatePackagePolicy).toHaveBeenCalledWith(
'nginx-1',
expect.objectContaining({
policy_ids: ['agent-policy-1', 'agent-policy-2'],
})
);
});
});
});

View file

@ -6,7 +6,7 @@
*/
import React, { useState, useEffect, useCallback, useMemo, memo } from 'react';
import { isEmpty, omit } from 'lodash';
import { isEmpty } from 'lodash';
import { useRouteMatch } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
@ -48,28 +48,28 @@ import {
StepDefinePackagePolicy,
} from '../create_package_policy_page/components';
import {
AGENTLESS_POLICY_ID,
HIDDEN_API_REFERENCE_PACKAGES,
} from '../../../../../../common/constants';
import type { PackagePolicyEditExtensionComponentProps } from '../../../types';
import { AGENTLESS_POLICY_ID } from '../../../../../../common/constants';
import type { AgentPolicy, PackagePolicyEditExtensionComponentProps } from '../../../types';
import { ExperimentalFeaturesService, pkgKeyFromPackageInfo } from '../../../services';
import { generateUpdatePackagePolicyDevToolsRequest } from '../services';
import {
getInheritedNamespace,
getRootPrivilegedDataStreams,
isRootPrivilegesRequired,
} from '../../../../../../common/services';
import { RootPrivilegesCallout } from '../create_package_policy_page/single_page_layout/root_callout';
import { StepsWithLessPadding } from '../create_package_policy_page/single_page_layout';
import { UpgradeStatusCallout } from './components';
import { usePackagePolicyWithRelatedData, useHistoryBlock } from './hooks';
import { getNewSecrets } from './utils';
import { usePackagePolicySteps } from './hooks';
export const EditPackagePolicyPage = memo(() => {
const {
params: { packagePolicyId },
params: { packagePolicyId, policyId },
} = useRouteMatch<{ policyId: string; packagePolicyId: string }>();
const packagePolicy = useGetOnePackagePolicy(packagePolicyId);
@ -82,6 +82,7 @@ export const EditPackagePolicyPage = memo(() => {
return (
<EditPackagePolicyForm
packagePolicyId={packagePolicyId}
policyId={policyId}
// If an extension opts in to this `useLatestPackageVersion` flag, we want to display
// the edit form in an "upgrade" state regardless of whether the user intended to
// "edit" their policy or "upgrade" it. This ensures the new policy generated will be
@ -95,16 +96,18 @@ export const EditPackagePolicyForm = memo<{
packagePolicyId: string;
forceUpgrade?: boolean;
from?: EditPackagePolicyFrom;
}>(({ packagePolicyId, forceUpgrade = false, from = 'edit' }) => {
policyId?: string;
}>(({ packagePolicyId, policyId, forceUpgrade = false, from = 'edit' }) => {
const { application, notifications } = useStartServices();
const {
agents: { enabled: isFleetEnabled },
} = useConfig();
const { getHref } = useLink();
const { enableReusableIntegrationPolicies } = ExperimentalFeaturesService.get();
const {
// data
agentPolicies,
agentPolicies: existingAgentPolicies,
isLoadingData,
loadingError,
packagePolicy,
@ -135,15 +138,19 @@ export const EditPackagePolicyForm = memo<{
return getNewSecrets({ packageInfo, packagePolicy });
}, [packageInfo, packagePolicy]);
const policyIds = agentPolicies.map((policy) => policy.id);
const [agentPolicies, setAgentPolicies] = useState<AgentPolicy[]>([]);
const [isFirstLoad, setIsFirstLoad] = useState<boolean>(true);
const [newAgentPolicyName, setNewAgentPolicyName] = useState<string | undefined>();
const [hasAgentPolicyError, setHasAgentPolicyError] = useState<boolean>(false);
// Retrieve agent count
const [agentCount, setAgentCount] = useState<number>(0);
useEffect(() => {
const getAgentCount = async () => {
let count = 0;
for (const policyId of policyIds) {
const { data } = await sendGetAgentStatus({ policyId });
for (const id of packagePolicy.policy_ids) {
const { data } = await sendGetAgentStatus({ policyId: id });
if (data?.results.total) {
count += data.results.total;
}
@ -151,10 +158,10 @@ export const EditPackagePolicyForm = memo<{
setAgentCount(count);
};
if (isFleetEnabled && policyIds.length > 0) {
if (isFleetEnabled && packagePolicy.policy_ids.length > 0) {
getAgentCount();
}
}, [policyIds, isFleetEnabled]);
}, [packagePolicy.policy_ids, isFleetEnabled]);
const handleExtensionViewOnChange = useCallback<
PackagePolicyEditExtensionComponentProps['onChange']
@ -175,39 +182,90 @@ export const EditPackagePolicyForm = memo<{
// if `from === 'edit'` then it links back to Policy Details
// if `from === 'package-edit'`, or `upgrade-from-integrations-policy-list` then it links back to the Integration Policy List
const cancelUrl = useMemo((): string => {
if (packageInfo && policyIds.length > 0) {
if (packageInfo && policyId) {
return from === 'package-edit'
? getHref('integration_details_policies', {
pkgkey: pkgKeyFromPackageInfo(packageInfo!),
})
: getHref('policy_details', { policyId: policyIds[0] });
: getHref('policy_details', { policyId });
}
return '/';
}, [from, getHref, packageInfo, policyIds]);
}, [from, getHref, packageInfo, policyId]);
const successRedirectPath = useMemo(() => {
if (packageInfo && policyIds.length > 0) {
if (packageInfo && policyId) {
return from === 'package-edit' || from === 'upgrade-from-integrations-policy-list'
? getHref('integration_details_policies', {
pkgkey: pkgKeyFromPackageInfo(packageInfo!),
})
: getHref('policy_details', { policyId: policyIds[0] });
: getHref('policy_details', { policyId });
}
return '/';
}, [from, getHref, packageInfo, policyIds]);
}, [from, getHref, packageInfo, policyId]);
useHistoryBlock(isEdited);
useEffect(() => {
if (existingAgentPolicies.length > 0 && isFirstLoad) {
setIsFirstLoad(false);
setAgentPolicies(existingAgentPolicies);
}
}, [existingAgentPolicies, isFirstLoad]);
const agentPoliciesToAdd = useMemo(
() => [
...agentPolicies
.filter(
(policy) =>
!existingAgentPolicies.find((existingPolicy) => existingPolicy.id === policy.id)
)
.map((policy) => policy.name),
...(newAgentPolicyName ? [newAgentPolicyName] : []),
],
[agentPolicies, existingAgentPolicies, newAgentPolicyName]
);
const agentPoliciesToRemove = useMemo(
() =>
existingAgentPolicies
.filter(
(existingPolicy) => !agentPolicies.find((policy) => policy.id === existingPolicy.id)
)
.map((policy) => policy.name),
[agentPolicies, existingAgentPolicies]
);
const onSubmit = async () => {
if (formState === 'VALID' && hasErrors) {
setFormState('INVALID');
return;
}
if (agentCount !== 0 && !policyIds.includes(AGENTLESS_POLICY_ID) && formState !== 'CONFIRM') {
if (
(agentCount !== 0 || agentPoliciesToAdd.length > 0 || agentPoliciesToRemove.length > 0) &&
!packagePolicy.policy_ids.includes(AGENTLESS_POLICY_ID) &&
formState !== 'CONFIRM'
) {
setFormState('CONFIRM');
return;
}
const { error } = await savePackagePolicy();
let newPolicyId;
try {
setFormState('LOADING');
newPolicyId = await createAgentPolicyIfNeeded();
} catch (e) {
setFormState('VALID');
notifications.toasts.addError(e, {
title: i18n.translate('xpack.fleet.createAgentPolicy.errorNotificationTitle', {
defaultMessage: 'Unable to create agent policy',
}),
});
return;
}
const { error } = await savePackagePolicy({
policy_ids: newPolicyId
? [...packagePolicy.policy_ids, newPolicyId]
: packagePolicy.policy_ids,
});
if (!error) {
setIsEdited(false);
application.navigateToUrl(successRedirectPath);
@ -311,7 +369,7 @@ export const EditPackagePolicyForm = memo<{
<>
{selectedTab === 0 && (
<StepDefinePackagePolicy
agentPolicies={agentPolicies}
namespacePlaceholder={getInheritedNamespace(agentPolicies)}
packageInfo={packageInfo}
packagePolicy={packagePolicy}
updatePackagePolicy={updatePackagePolicy}
@ -384,29 +442,38 @@ export const EditPackagePolicyForm = memo<{
</ExtensionWrapper>
);
const { showDevtoolsRequest: isShowDevtoolRequestExperimentEnabled } =
ExperimentalFeaturesService.get();
const showDevtoolsRequest =
!HIDDEN_API_REFERENCE_PACKAGES.includes(packageInfo?.name ?? '') &&
isShowDevtoolRequestExperimentEnabled;
const devtoolRequest = useMemo(
() =>
generateUpdatePackagePolicyDevToolsRequest(
packagePolicyId,
omit(packagePolicy, 'elasticsearch')
),
[packagePolicyId, packagePolicy]
);
const rootPrivilegedDataStreams = packageInfo ? getRootPrivilegedDataStreams(packageInfo) : [];
const agentPolicyBreadcrumb = useMemo(() => {
return existingAgentPolicies.length > 0
? existingAgentPolicies.find((policy) => policy.id === policyId) ?? existingAgentPolicies[0]
: { name: '', id: '' };
}, [existingAgentPolicies, policyId]);
const {
steps,
devToolsProps: { devtoolRequest, devtoolRequestDescription, showDevtoolsRequest },
createAgentPolicyIfNeeded,
} = usePackagePolicySteps({
configureStep: replaceConfigurePackage || configurePackage,
packageInfo,
existingAgentPolicies,
setHasAgentPolicyError,
updatePackagePolicy,
agentPolicies,
setAgentPolicies,
isLoadingData,
packagePolicy,
packagePolicyId,
setNewAgentPolicyName,
});
return (
<CreatePackagePolicySinglePageLayout {...layoutProps} data-test-subj="editPackagePolicy">
<EuiErrorBoundary>
{isLoadingData ? (
<Loading />
) : loadingError || isEmpty(agentPolicies) || !packageInfo ? (
) : loadingError || isEmpty(existingAgentPolicies) || !packageInfo ? (
<ErrorComponent
title={
<FormattedMessage
@ -424,12 +491,12 @@ export const EditPackagePolicyForm = memo<{
) : (
<>
<Breadcrumb
agentPolicyName={agentPolicies[0].name}
agentPolicyName={agentPolicyBreadcrumb.name}
from={from}
packagePolicyName={packagePolicy.name}
pkgkey={pkgKeyFromPackageInfo(packageInfo)}
pkgTitle={packageInfo.title}
policyId={policyIds[0]}
policyId={agentPolicyBreadcrumb.id}
/>
{formState === 'CONFIRM' && (
<ConfirmDeployAgentPolicyModal
@ -437,6 +504,8 @@ export const EditPackagePolicyForm = memo<{
agentPolicies={agentPolicies}
onConfirm={onSubmit}
onCancel={() => setFormState('VALID')}
agentPoliciesToAdd={agentPoliciesToAdd}
agentPoliciesToRemove={agentPoliciesToRemove}
/>
)}
{packageInfo && isRootPrivilegesRequired(packageInfo) ? (
@ -451,14 +520,20 @@ export const EditPackagePolicyForm = memo<{
<EuiSpacer size="xxl" />
</>
)}
{replaceConfigurePackage || configurePackage}
{enableReusableIntegrationPolicies ? (
<StepsWithLessPadding steps={steps} />
) : (
replaceConfigurePackage || configurePackage
)}
{/* Extra space to accomodate the EuiBottomBar height */}
<EuiSpacer size="xxl" />
<EuiSpacer size="xxl" />
<EuiBottomBar>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
{agentPolicies && packageInfo && formState === 'INVALID' ? (
{agentPolicies &&
packageInfo &&
(formState === 'INVALID' || hasAgentPolicyError) ? (
<FormattedMessage
id="xpack.fleet.createPackagePolicy.errorOnSaveText"
defaultMessage="Your integration policy has errors. Please fix them before saving."
@ -482,13 +557,8 @@ export const EditPackagePolicyForm = memo<{
btnProps={{
color: 'text',
}}
description={i18n.translate(
'xpack.fleet.editPackagePolicy.devtoolsRequestDescription',
{
defaultMessage: 'This Kibana request updates a package policy.',
}
)}
request={devtoolRequest}
description={devtoolRequestDescription}
/>
</EuiFlexItem>
) : null}
@ -500,6 +570,8 @@ export const EditPackagePolicyForm = memo<{
isDisabled={
!canWriteIntegrationPolicies ||
formState !== 'VALID' ||
hasAgentPolicyError ||
!validationResults ||
(!isEdited && !isUpgrade)
}
tooltip={

View file

@ -14,7 +14,7 @@ import { EditPackagePolicyForm } from '../edit_package_policy_page';
export const UpgradePackagePolicyPage = memo(() => {
const {
params: { packagePolicyId },
params: { packagePolicyId, policyId },
} = useRouteMatch<{ policyId: string; packagePolicyId: string }>();
const { search } = useLocation();
@ -30,5 +30,12 @@ export const UpgradePackagePolicyPage = memo(() => {
from = 'upgrade-from-integrations-policy-list';
}
return <EditPackagePolicyForm packagePolicyId={packagePolicyId} from={from} forceUpgrade />;
return (
<EditPackagePolicyForm
packagePolicyId={packagePolicyId}
policyId={policyId}
from={from}
forceUpgrade
/>
);
});