diff --git a/x-pack/plugins/fleet/common/experimental_features.ts b/x-pack/plugins/fleet/common/experimental_features.ts index 0350af4f24620a..6233ef5f820cf1 100644 --- a/x-pack/plugins/fleet/common/experimental_features.ts +++ b/x-pack/plugins/fleet/common/experimental_features.ts @@ -31,6 +31,7 @@ export const allowedExperimentalValues = Object.freeze>( enablePackagesStateMachine: true, advancedPolicySettings: true, useSpaceAwareness: false, + enableReusableIntegrationPolicies: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/confirm_deploy_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/confirm_deploy_modal.tsx index 1ede87ba34c9b8..e19ec358abd645 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/confirm_deploy_modal.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/confirm_deploy_modal.tsx @@ -16,7 +16,7 @@ export const ConfirmDeployAgentPolicyModal: React.FunctionComponent<{ onConfirm: () => void; onCancel: () => void; agentCount: number; - agentPolicy: AgentPolicy; + agentPolicies: AgentPolicy[]; showUnprivilegedAgentsCallout?: boolean; unprivilegedAgentsCount?: number; dataStreams?: Array<{ name: string; title: string }>; @@ -24,7 +24,7 @@ export const ConfirmDeployAgentPolicyModal: React.FunctionComponent<{ onConfirm, onCancel, agentCount, - agentPolicy, + agentPolicies, showUnprivilegedAgentsCallout = false, unprivilegedAgentsCount = 0, dataStreams, @@ -66,11 +66,11 @@ export const ConfirmDeployAgentPolicyModal: React.FunctionComponent<{
{agentPolicy.name}, + policyNames: {agentPolicies.map((policy) => policy.name).join(', ')}, }} />
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/agent_policy_multi_select.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/agent_policy_multi_select.tsx new file mode 100644 index 00000000000000..4b10b2e2fc9ac7 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/agent_policy_multi_select.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 type { EuiComboBoxOptionOption } from '@elastic/eui'; +import { EuiComboBox } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import React, { useMemo } from 'react'; + +import type { PackageInfo } from '../../../../../../../../../common'; + +export interface Props { + isLoading: boolean; + agentPolicyMultiOptions: Array>; + selectedPolicyIds: string[]; + setSelectedPolicyIds: (policyIds: string[]) => void; + packageInfo?: PackageInfo; +} + +export const AgentPolicyMultiSelect: React.FunctionComponent = ({ + isLoading, + agentPolicyMultiOptions, + selectedPolicyIds, + setSelectedPolicyIds, +}) => { + const selectedOptions = useMemo(() => { + return agentPolicyMultiOptions.filter((option) => selectedPolicyIds.includes(option.key!)); + }, [agentPolicyMultiOptions, selectedPolicyIds]); + + return ( + { + setSelectedPolicyIds(newOptions.map((option: any) => option.key)); + }} + isClearable={true} + isLoading={isLoading} + /> + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.test.tsx index 193703a2c96eed..593d25ebaa97cd 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.test.tsx @@ -12,26 +12,8 @@ import type { TestRenderer } from '../../../../../../../mock'; import { createFleetTestRendererMock } from '../../../../../../../mock'; import type { AgentPolicy, NewPackagePolicy, PackageInfo } from '../../../../../types'; -import { useGetPackagePolicies } from '../../../../../hooks'; - import { StepDefinePackagePolicy } from './step_define_package_policy'; -jest.mock('../../../../../hooks', () => { - return { - ...jest.requireActual('../../../../../hooks'), - useGetPackagePolicies: jest.fn().mockReturnValue({ - data: { - items: [{ name: 'nginx-1' }, { name: 'other-policy' }], - }, - isLoading: false, - }), - useFleetStatus: jest.fn().mockReturnValue({ isReady: true } as any), - sendGetStatus: jest - .fn() - .mockResolvedValue({ data: { isReady: true, missing_requirements: [] } }), - }; -}); - describe('StepDefinePackagePolicy', () => { const packageInfo: PackageInfo = { name: 'apache', @@ -63,18 +45,32 @@ describe('StepDefinePackagePolicy', () => { }, ], }; - const agentPolicy: AgentPolicy = { - id: 'agent-policy-1', - namespace: 'ns', - 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: 'ns', + 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, + }, + ]; let packagePolicy: NewPackagePolicy; const mockUpdatePackagePolicy = jest.fn().mockImplementation((val: any) => { packagePolicy = { @@ -96,7 +92,7 @@ describe('StepDefinePackagePolicy', () => { const render = () => (renderResult = testRenderer.render( { waitFor(() => { expect(renderResult.getByRole('switch')).toHaveAttribute('aria-label', 'Advanced var'); + expect(renderResult.getByTestId('packagePolicyNamespaceInput')).toHaveAttribute( + 'placeholder', + 'ns' + ); }); }); }); - it('should set incremented name if other package policies exist', () => { - (useGetPackagePolicies as jest.MockedFunction).mockReturnValueOnce({ - data: { - items: [ - { name: 'apache-1' }, - { name: 'apache-2' }, - { name: 'apache-9' }, - { name: 'apache-10' }, - ], - }, - isLoading: false, - }); - - render(); - - waitFor(() => { - expect(renderResult.getByDisplayValue('apache-11')).toBeInTheDocument(); - }); - }); - describe('update', () => { describe('when package vars are introduced in a new package version', () => { it('should display new package vars', () => { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.tsx index 4f50a0c1cc5f27..add3cf72485f32 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.tsx @@ -48,7 +48,7 @@ const FormGroupResponsiveFields = styled(EuiDescribedFormGroup)` `; export const StepDefinePackagePolicy: React.FunctionComponent<{ - agentPolicy?: AgentPolicy; + agentPolicies?: AgentPolicy[]; packageInfo: PackageInfo; packagePolicy: NewPackagePolicy; updatePackagePolicy: (fields: Partial) => void; @@ -58,7 +58,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ noAdvancedToggle?: boolean; }> = memo( ({ - agentPolicy, + agentPolicies, packageInfo, packagePolicy, updatePackagePolicy, @@ -283,8 +283,9 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ } > { return { ...jest.requireActual('../../../../../hooks'), useGetAgentPolicies: jest.fn(), - useGetOutputs: jest.fn().mockResolvedValue({ - data: [], + useGetOutputs: jest.fn().mockReturnValue({ + data: { + items: [ + { + id: 'logstash-1', + type: 'logstash', + }, + ], + }, isLoading: false, }), - sendGetOneAgentPolicy: jest.fn().mockResolvedValue({ - data: { item: { id: 'policy-1' } }, - }), + sendBulkGetAgentPolicies: jest.fn().mockImplementation((ids) => + Promise.resolve({ + data: { items: ids.map((id: string) => ({ id, package_policies: [] })) }, + }) + ), useFleetStatus: jest.fn().mockReturnValue({ isReady: true } as any), sendGetFleetStatus: jest .fn() .mockResolvedValue({ data: { isReady: true, missing_requirements: [] } }), + useGetPackagePolicies: jest.fn().mockImplementation((query) => ({ + data: { + items: query.kuery.includes('osquery_manager') + ? [{ policy_ids: ['policy-1'] }] + : query.kuery.includes('apm') + ? [{ policy_ids: ['policy-2'] }] + : [], + }, + error: undefined, + isLoading: false, + resendRequest: jest.fn(), + })), }; }); @@ -41,21 +66,21 @@ describe('step select agent policy', () => { let testRenderer: TestRenderer; let renderResult: ReturnType; const mockSetHasAgentPolicyError = jest.fn(); - const updateAgentPolicyMock = jest.fn(); - const render = () => + const updateAgentPoliciesMock = jest.fn(); + const render = (packageInfo?: PackageInfo, selectedAgentPolicyId?: string) => (renderResult = testRenderer.render( )); beforeEach(() => { testRenderer = createFleetTestRendererMock(); - updateAgentPolicyMock.mockReset(); + updateAgentPoliciesMock.mockReset(); }); test('should not select agent policy by default if multiple exists', async () => { @@ -90,10 +115,151 @@ describe('step select agent policy', () => { } as any); render(); - await act(async () => {}); // Needed as updateAgentPolicy is called after multiple useEffect + await act(async () => {}); // Needed as updateAgentPolicies is called after multiple useEffect await act(async () => { - expect(updateAgentPolicyMock).toBeCalled(); - expect(updateAgentPolicyMock).toBeCalledWith({ id: 'policy-1' }); + expect(updateAgentPoliciesMock).toBeCalled(); + expect(updateAgentPoliciesMock).toBeCalledWith([{ id: 'policy-1', package_policies: [] }]); + }); + }); + + describe('multiple agent policies', () => { + beforeEach(() => { + jest + .spyOn(ExperimentalFeaturesService, 'get') + .mockReturnValue({ enableReusableIntegrationPolicies: true }); + + useGetAgentPoliciesMock.mockReturnValue({ + data: { + items: [ + { id: 'policy-1', name: 'Policy 1' }, + { id: 'policy-2', name: 'Policy 2' }, + ], + }, + error: undefined, + isLoading: false, + resendRequest: jest.fn(), + } as any); + }); + + test('should select agent policy by default if one exists', async () => { + useGetAgentPoliciesMock.mockReturnValueOnce({ + data: { items: [{ id: 'policy-1', name: 'Policy 1' }] }, + error: undefined, + isLoading: false, + resendRequest: jest.fn(), + } as any); + + render(); + await act(async () => {}); // Needed as updateAgentPolicies is called after multiple useEffect + await act(async () => { + expect(updateAgentPoliciesMock).toBeCalledWith([{ id: 'policy-1', package_policies: [] }]); + }); + }); + + test('should not select agent policy by default if multiple exists', async () => { + useGetAgentPoliciesMock.mockReturnValue({ + data: { + items: [ + { id: 'policy-1', name: 'Policy 1' }, + { id: 'policy-2', name: 'Policy 2' }, + ], + }, + error: undefined, + isLoading: false, + resendRequest: jest.fn(), + } as any); + + render(); + + await act(async () => { + const select = renderResult.container.querySelector( + '[data-test-subj="agentPolicyMultiSelect"]' + ); + expect((select as any)?.value).toEqual(undefined); + + expect(renderResult.getByText('An agent policy is required.')).toBeVisible(); + }); + }); + + test('should select agent policy if pre selected', async () => { + 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: [] }]); + }); + }); + + test('should select multiple agent policies', async () => { + const result = render(); + expect(result.getByTestId('agentPolicyMultiSelect')).toBeInTheDocument(); + await act(async () => { + result.getByTestId('comboBoxToggleListButton').click(); + }); + expect(result.getAllByTestId('agentPolicyMultiItem').length).toBe(2); + await act(async () => { + result.getByText('Policy 1').click(); + }); + await act(async () => { + result.getByText('Policy 2').click(); + }); + expect(updateAgentPoliciesMock).toBeCalledWith([ + { id: 'policy-1', package_policies: [] }, + { id: 'policy-2', package_policies: [] }, + ]); + }); + + test('should disable option if agent policy has limited package', async () => { + useGetAgentPoliciesMock.mockReturnValue({ + data: { + items: [ + { id: 'policy-1', name: 'Policy 1' }, + { id: 'policy-2', name: 'Policy 2' }, + { id: 'policy-3', name: 'Policy 3' }, + ], + }, + error: undefined, + isLoading: false, + resendRequest: jest.fn(), + } as any); + const result = render({ + name: 'osquery_manager', + policy_templates: [{ multiple: false }], + } as any); + expect(result.getByTestId('agentPolicyMultiSelect')).toBeInTheDocument(); + await act(async () => { + result.getByTestId('comboBoxToggleListButton').click(); + }); + expect( + result.getByText('Policy 1').closest('[data-test-subj="agentPolicyMultiItem"]') + ).toBeDisabled(); + }); + + test('should disable option if agent policy has apm package and logstash output', async () => { + useGetAgentPoliciesMock.mockReturnValue({ + data: { + items: [ + { id: 'policy-1', name: 'Policy 1' }, + { id: 'policy-2', name: 'Policy 2', data_output_id: 'logstash-1' }, + { id: 'policy-3', name: 'Policy 3' }, + ], + }, + error: undefined, + isLoading: false, + resendRequest: jest.fn(), + } as any); + const result = render({ + name: 'apm', + } as any); + expect(result.getByTestId('agentPolicyMultiSelect')).toBeInTheDocument(); + await act(async () => { + result.getByTestId('comboBoxToggleListButton').click(); + }); + expect( + result.getByText('Policy 2').closest('[data-test-subj="agentPolicyMultiItem"]') + ).toBeDisabled(); + expect( + result.getByTitle('Policy 2').querySelector('[data-euiicon-type="warningFilled"]') + ).toBeInTheDocument(); }); }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_agent_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_agent_policy.tsx index 1524c3590c82b8..262efea5e32fbf 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_agent_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_agent_policy.tsx @@ -9,7 +9,8 @@ import React, { useEffect, useState, useMemo, useCallback } from 'react'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { EuiSuperSelectOption } from '@elastic/eui'; +import type { EuiComboBoxOptionOption, EuiSuperSelectOption } from '@elastic/eui'; +import { EuiIcon, EuiToolTip } from '@elastic/eui'; import { EuiSuperSelect } from '@elastic/eui'; import { EuiFlexGroup, @@ -23,13 +24,17 @@ import { import { Error } from '../../../../../components'; import type { AgentPolicy, Output, PackageInfo } from '../../../../../types'; -import { isPackageLimited, doesAgentPolicyAlreadyIncludePackage } from '../../../../../services'; +import { + isPackageLimited, + doesAgentPolicyAlreadyIncludePackage, + ExperimentalFeaturesService, +} from '../../../../../services'; import { useGetAgentPolicies, useGetOutputs, - sendGetOneAgentPolicy, useFleetStatus, useGetPackagePolicies, + sendBulkGetAgentPolicies, } from '../../../../../hooks'; import { FLEET_APM_PACKAGE, @@ -38,6 +43,8 @@ import { PACKAGE_POLICY_SAVED_OBJECT_TYPE, } from '../../../../../../../../common/constants'; +import { AgentPolicyMultiSelect } from './components/agent_policy_multi_select'; + const AgentPolicyFormRow = styled(EuiFormRow)` .euiFormRow__label { width: 100%; @@ -126,7 +133,7 @@ function useAgentPoliciesOptions(packageInfo?: PackageInfo) { @@ -147,11 +154,55 @@ function useAgentPoliciesOptions(packageInfo?: PackageInfo) { ] ); + const agentPolicyMultiOptions: Array> = 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 ? ( + + } + > + + + ) : 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, }; } @@ -163,14 +214,14 @@ function doesAgentPolicyHaveLimitedPackage(policy: AgentPolicy, pkgInfo: Package export const StepSelectAgentPolicy: React.FunctionComponent<{ packageInfo?: PackageInfo; - agentPolicy: AgentPolicy | undefined; - updateAgentPolicy: (agentPolicy: AgentPolicy | undefined) => void; + agentPolicies: AgentPolicy[]; + updateAgentPolicies: (agentPolicies: AgentPolicy[]) => void; setHasAgentPolicyError: (hasError: boolean) => void; selectedAgentPolicyId?: string; }> = ({ packageInfo, - agentPolicy, - updateAgentPolicy: updateSelectedAgentPolicy, + agentPolicies, + updateAgentPolicies: updateSelectedAgentPolicies, setHasAgentPolicyError, selectedAgentPolicyId, }) => { @@ -178,69 +229,101 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{ const [selectedAgentPolicyError, setSelectedAgentPolicyError] = useState(); - const { isLoading, agentPoliciesError, agentPolicyOptions, agentPolicies } = - useAgentPoliciesOptions(packageInfo); - // Selected agent policy state - const [selectedPolicyId, setSelectedPolicyId] = useState( - agentPolicy?.id ?? - (selectedAgentPolicyId || (agentPolicies.length === 1 ? agentPolicies[0].id : undefined)) - ); + const { enableReusableIntegrationPolicies } = ExperimentalFeaturesService.get(); + + const { + isLoading, + agentPoliciesError, + agentPolicyOptions, + agentPolicyMultiOptions, + agentPolicies: existingAgentPolicies, + } = useAgentPoliciesOptions(packageInfo); + + const [selectedPolicyIds, setSelectedPolicyIds] = useState([]); const [isLoadingSelectedAgentPolicy, setIsLoadingSelectedAgentPolicy] = useState(false); - const [selectedAgentPolicy, setSelectedAgentPolicy] = useState( - agentPolicy - ); + const [selectedAgentPolicies, setSelectedAgentPolicies] = useState(agentPolicies); - const updateAgentPolicy = useCallback( - (selectedPolicy: AgentPolicy | undefined) => { - setSelectedAgentPolicy(selectedPolicy); - updateSelectedAgentPolicy(selectedPolicy); + const updateAgentPolicies = useCallback( + (selectedPolicies: AgentPolicy[]) => { + setSelectedAgentPolicies(selectedPolicies); + updateSelectedAgentPolicies(selectedPolicies); }, - [updateSelectedAgentPolicy] + [updateSelectedAgentPolicies] ); // Update parent selected agent policy state useEffect(() => { const fetchAgentPolicyInfo = async () => { - if (selectedPolicyId) { + if (selectedPolicyIds.length > 0) { setIsLoadingSelectedAgentPolicy(true); - const { data, error } = await sendGetOneAgentPolicy(selectedPolicyId); + const { data, error } = await sendBulkGetAgentPolicies(selectedPolicyIds, { full: true }); if (error) { setSelectedAgentPolicyError(error); - updateAgentPolicy(undefined); - } else if (data && data.item) { + updateAgentPolicies([]); + } else if (data && data.items) { setSelectedAgentPolicyError(undefined); - updateAgentPolicy(data.item); + updateAgentPolicies(data.items); } setIsLoadingSelectedAgentPolicy(false); } else { setSelectedAgentPolicyError(undefined); - updateAgentPolicy(undefined); + updateAgentPolicies([]); } }; - if (!agentPolicy || selectedPolicyId !== agentPolicy.id) { + const agentPoliciesHaveAllSelectedIds = selectedPolicyIds.every((id) => + agentPolicies.map((policy) => policy.id).includes(id) + ); + if (agentPolicies.length === 0 || !agentPoliciesHaveAllSelectedIds) { fetchAgentPolicyInfo(); + } else if (agentPoliciesHaveAllSelectedIds && selectedPolicyIds.length < agentPolicies.length) { + setSelectedAgentPolicyError(undefined); + updateAgentPolicies(agentPolicies.filter((policy) => selectedPolicyIds.includes(policy.id))); } - }, [selectedPolicyId, agentPolicy, updateAgentPolicy]); + }, [selectedPolicyIds, agentPolicies, updateAgentPolicies]); // Try to select default agent policy useEffect(() => { - if (!selectedPolicyId && agentPolicies.length && agentPolicyOptions.length) { - const enabledOptions = agentPolicyOptions.filter((option) => !option.disabled); - if (enabledOptions.length === 1) { - setSelectedPolicyId(enabledOptions[0].value as string | undefined); + if ( + selectedPolicyIds.length === 0 && + existingAgentPolicies.length && + (enableReusableIntegrationPolicies + ? agentPolicyMultiOptions.length + : agentPolicyOptions.length) + ) { + if (enableReusableIntegrationPolicies) { + const enabledOptions = agentPolicyMultiOptions.filter((option) => !option.disabled); + if (enabledOptions.length === 1) { + setSelectedPolicyIds([enabledOptions[0].key!]); + } else if (selectedAgentPolicyId) { + setSelectedPolicyIds([selectedAgentPolicyId]); + } + } else { + const enabledOptions = agentPolicyOptions.filter((option) => !option.disabled); + if (enabledOptions.length === 1) { + setSelectedPolicyIds([enabledOptions[0].value]); + } else if (selectedAgentPolicyId) { + setSelectedPolicyIds([selectedAgentPolicyId]); + } } } - }, [agentPolicies, agentPolicyOptions, selectedPolicyId]); + }, [ + agentPolicyOptions, + agentPolicyMultiOptions, + enableReusableIntegrationPolicies, + selectedAgentPolicyId, + selectedPolicyIds, + existingAgentPolicies, + ]); // Bubble up any issues with agent policy selection useEffect(() => { - if (selectedPolicyId && !selectedAgentPolicyError) { + if (selectedPolicyIds.length > 0 && !selectedAgentPolicyError) { setHasAgentPolicyError(false); } else setHasAgentPolicyError(true); - }, [selectedAgentPolicyError, selectedPolicyId, setHasAgentPolicyError]); + }, [selectedAgentPolicyError, selectedPolicyIds, setHasAgentPolicyError]); const onChange = useCallback( - (newValue: string) => setSelectedPolicyId(newValue === '' ? undefined : newValue), + (newValue: string) => setSelectedPolicyIds(newValue === '' ? [] : [newValue]), [] ); @@ -298,24 +381,28 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{ } helpText={ - isFleetReady && selectedPolicyId && !isLoadingSelectedAgentPolicy ? ( + isFleetReady && selectedPolicyIds.length > 0 && !isLoadingSelectedAgentPolicy ? ( acc + (curr.agents ?? 0), + 0 + ), }} /> ) : null } isInvalid={Boolean( - !selectedPolicyId || + selectedPolicyIds.length === 0 || !packageInfo || - (selectedAgentPolicy && - doesAgentPolicyHaveLimitedPackage(selectedAgentPolicy, packageInfo)) + selectedAgentPolicies.every((selectedAgentPolicy) => + doesAgentPolicyHaveLimitedPackage(selectedAgentPolicy, packageInfo) + ) )} error={ - !selectedPolicyId ? ( + selectedPolicyIds.length === 0 ? ( - + {enableReusableIntegrationPolicies ? ( + + ) : ( + + )} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_hosts.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_hosts.test.tsx index 89de7ba44b3e60..70fb22fcebdf1c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_hosts.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_hosts.test.tsx @@ -24,9 +24,11 @@ jest.mock('../../../../../hooks', () => { data: [], isLoading: false, }), - sendGetOneAgentPolicy: jest.fn().mockResolvedValue({ - data: { item: { id: 'policy-1', name: 'Agent policy 1' } }, - }), + sendGetOneAgentPolicy: jest.fn().mockImplementation((id) => + Promise.resolve({ + data: { item: { id, name: `Agent policy ${id}` } }, + }) + ), }; }); @@ -44,18 +46,32 @@ describe('StepSelectHosts', () => { status: 'not_installed', vars: [], }; - const agentPolicy: AgentPolicy = { - id: 'agent-policy-1', - namespace: 'default', - name: 'Agent policy 1', - is_managed: false, - status: 'active', - updated_at: '', - updated_by: '', - revision: 1, - package_policies: [], - is_protected: false, - }; + const agentPolicies: AgentPolicy[] = [ + { + id: '1', + namespace: 'default', + name: 'Agent policy 1', + is_managed: false, + status: 'active', + updated_at: '', + updated_by: '', + revision: 1, + package_policies: [], + is_protected: false, + }, + { + id: '2', + namespace: 'default', + name: 'Agent policy 2', + is_managed: false, + status: 'active', + updated_at: '', + updated_by: '', + revision: 1, + package_policies: [], + is_protected: false, + }, + ]; const newAgentPolicy = { name: '', namespace: 'default', @@ -67,8 +83,8 @@ describe('StepSelectHosts', () => { const render = () => (renderResult = testRenderer.render( { it('should display tabs with New hosts selected when agent policies exist', () => { (useGetAgentPolicies as jest.MockedFunction).mockReturnValue({ data: { - items: [{ id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' }], + items: [{ id: '1', name: 'Agent policy 1', namespace: 'default' }], }, }); @@ -110,7 +126,7 @@ describe('StepSelectHosts', () => { waitFor(() => { expect(renderResult.getByRole('tablist')).toBeInTheDocument(); - expect(renderResult.getByText('Agent policy 2')).toBeInTheDocument(); + expect(renderResult.getByText('Agent policy 3')).toBeInTheDocument(); }); expect(renderResult.getByText('New hosts').closest('button')).toHaveAttribute( 'aria-selected', @@ -121,7 +137,7 @@ describe('StepSelectHosts', () => { it('should display dropdown with agent policy selected when Existing hosts selected', async () => { (useGetAgentPolicies as jest.MockedFunction).mockReturnValue({ data: { - items: [{ id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' }], + items: [{ id: '1', name: 'Agent policy 1', namespace: 'default' }], }, }); @@ -143,8 +159,8 @@ describe('StepSelectHosts', () => { (useGetAgentPolicies as jest.MockedFunction).mockReturnValue({ data: { items: [ - { id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' }, - { id: 'agent-policy-2', name: 'Agent policy 2', namespace: 'default' }, + { id: '1', name: 'Agent policy 1', namespace: 'default' }, + { id: '2', name: 'Agent policy 2', namespace: 'default' }, ], }, }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_hosts.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_hosts.tsx index 87ef9ac864bfdb..a4b4f5a3e0970e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_hosts.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_select_hosts.tsx @@ -32,8 +32,8 @@ const StyledEuiTabbedContent = styled(EuiTabbedContent)` `; interface Props { - agentPolicy: AgentPolicy | undefined; - updateAgentPolicy: (u: AgentPolicy | undefined) => void; + agentPolicies: AgentPolicy[]; + updateAgentPolicies: (u: AgentPolicy[]) => void; newAgentPolicy: Partial; updateNewAgentPolicy: (u: Partial) => void; withSysMonitoring: boolean; @@ -46,8 +46,8 @@ interface Props { } export const StepSelectHosts: React.FunctionComponent = ({ - agentPolicy, - updateAgentPolicy, + agentPolicies, + updateAgentPolicies, newAgentPolicy, updateNewAgentPolicy, withSysMonitoring, @@ -58,7 +58,7 @@ export const StepSelectHosts: React.FunctionComponent = ({ updateSelectedTab, selectedAgentPolicyId, }) => { - let agentPolicies: AgentPolicy[] = []; + let existingAgentPolicies: AgentPolicy[] = []; const { data: agentPoliciesData, error: err } = useGetAgentPolicies({ page: 1, perPage: SO_SEARCH_LIMIT, @@ -71,19 +71,19 @@ export const StepSelectHosts: React.FunctionComponent = ({ // eslint-disable-next-line no-console console.debug('Could not retrieve agent policies'); } - agentPolicies = useMemo( + existingAgentPolicies = useMemo( () => agentPoliciesData?.items.filter((policy) => !policy.is_managed) || [], [agentPoliciesData?.items] ); useEffect(() => { - if (agentPolicies.length > 0) { + if (existingAgentPolicies.length > 0) { updateNewAgentPolicy({ ...newAgentPolicy, - name: incrementPolicyName(agentPolicies), + name: incrementPolicyName(existingAgentPolicies), }); } - }, [agentPolicies.length]); // eslint-disable-line react-hooks/exhaustive-deps + }, [existingAgentPolicies.length]); // eslint-disable-line react-hooks/exhaustive-deps const tabs = [ { @@ -107,8 +107,8 @@ export const StepSelectHosts: React.FunctionComponent = ({ content: ( @@ -119,7 +119,7 @@ export const StepSelectHosts: React.FunctionComponent = ({ const handleOnTabClick = (tab: EuiTabbedContentTab) => updateSelectedTab(tab.id as SelectedPolicyTab); - return agentPolicies.length > 0 ? ( + return existingAgentPolicies.length > 0 ? ( { expect(renderResult.result.current.packagePolicy).toEqual({ id: 'new-id', - policy_id: '', - policy_ids: [''], + policy_ids: [], namespace: 'newspace', description: '', enabled: true, @@ -147,8 +146,7 @@ describe('useOnSubmit', () => { inputs: [], name: 'apache-1', namespace: '', - policy_id: '', - policy_ids: [''], + policy_ids: [], package: { name: 'apache', title: 'Apache', @@ -193,8 +191,7 @@ describe('useOnSubmit', () => { inputs: [], name: 'apache-11', namespace: '', - policy_id: '', - policy_ids: [''], + policy_ids: [], package: { name: 'apache', title: 'Apache', diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx index 2f37ffea1b2149..be80a966972691 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx @@ -9,6 +9,8 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { safeLoad } from 'js-yaml'; +import { isEqual } from 'lodash'; + import type { AgentPolicy, NewPackagePolicy, @@ -121,7 +123,7 @@ export function useOnSubmit({ // Used to initialize the package policy once const isInitializedRef = useRef(false); - const [agentPolicy, setAgentPolicy] = useState(); + const [agentPolicies, setAgentPolicies] = useState([]); // New package policy state const [packagePolicy, setPackagePolicy] = useState({ ...DEFAULT_PACKAGE_POLICY, @@ -136,22 +138,25 @@ export function useOnSubmit({ useAgentless(); // Update agent policy method - const updateAgentPolicy = useCallback( - (updatedAgentPolicy: AgentPolicy | undefined) => { - if (updatedAgentPolicy) { - setAgentPolicy(updatedAgentPolicy); + const updateAgentPolicies = useCallback( + (updatedAgentPolicies: AgentPolicy[]) => { + if (isEqual(updatedAgentPolicies, agentPolicies)) { + return; + } + if (updatedAgentPolicies.length > 0) { + setAgentPolicies(updatedAgentPolicies); if (packageInfo) { setHasAgentPolicyError(false); } } else { setHasAgentPolicyError(true); - setAgentPolicy(undefined); + setAgentPolicies([]); } // eslint-disable-next-line no-console - console.debug('Agent policy updated', updatedAgentPolicy); + console.debug('Agent policy updated', updatedAgentPolicies); }, - [packageInfo, setAgentPolicy] + [packageInfo, agentPolicies] ); // Update package policy validation const updatePackagePolicyValidation = useCallback( @@ -221,7 +226,7 @@ export function useOnSubmit({ updatePackagePolicy( packageToPackagePolicy( packageInfo, - agentPolicy?.id || '', + agentPolicies.map((policy) => policy.id), '', DEFAULT_PACKAGE_POLICY.name || incrementedName, DEFAULT_PACKAGE_POLICY.description, @@ -231,15 +236,21 @@ export function useOnSubmit({ setIsInitialized(true); } init(); - }, [packageInfo, agentPolicy, updatePackagePolicy, integrationToEnable, isInitialized]); + }, [packageInfo, agentPolicies, updatePackagePolicy, integrationToEnable, isInitialized]); useEffect(() => { - if (agentPolicy && !packagePolicy.policy_ids.includes(agentPolicy.id)) { + if ( + agentPolicies.length > 0 && + !isEqual( + agentPolicies.map((policy) => policy.id), + packagePolicy.policy_ids + ) + ) { updatePackagePolicy({ - policy_ids: [agentPolicy.id], + policy_ids: agentPolicies.map((policy) => policy.id), }); } - }, [packagePolicy, agentPolicy, updatePackagePolicy]); + }, [packagePolicy, agentPolicies, updatePackagePolicy]); const onSaveNavigate = useOnSaveNavigate({ packagePolicy, @@ -295,7 +306,7 @@ export function useOnSubmit({ packagePolicy, withSysMonitoring, }); - setAgentPolicy(createdPolicy); + setAgentPolicies([createdPolicy]); updatePackagePolicy({ policy_ids: [createdPolicy.id] }); } catch (e) { setFormState('VALID'); @@ -361,7 +372,7 @@ export function useOnSubmit({ setSavedPackagePolicy(data!.item); const promptForAgentEnrollment = - !(agentCount && agentPolicy) && hasFleetAddAgentsPrivileges; + !(agentCount && agentPolicies.length > 0) && hasFleetAddAgentsPrivileges; if (promptForAgentEnrollment && hasAzureArmTemplate) { setFormState('SUBMITTED_AZURE_ARM_TEMPLATE'); return; @@ -389,9 +400,9 @@ export function useOnSubmit({ }), text: promptForAgentEnrollment ? i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationMessage', { - defaultMessage: `Fleet will deploy updates to all agents that use the ''{agentPolicyName}'' policy.`, + defaultMessage: `Fleet will deploy updates to all agents that use the ''{agentPolicyNames}'' policies.`, values: { - agentPolicyName: agentPolicy!.name, + agentPolicyNames: agentPolicies.map((policy) => policy.name).join(', '), }, }) : undefined, @@ -431,15 +442,15 @@ export function useOnSubmit({ newAgentPolicy, updatePackagePolicy, notifications.toasts, - agentPolicy, + agentPolicies, onSaveNavigate, confirmForceInstall, ] ); return { - agentPolicy, - updateAgentPolicy, + agentPolicies, + updateAgentPolicies, packagePolicy, updatePackagePolicy, savedPackagePolicy, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts index dd2a1dcc81c79b..5099cdde2277c5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts @@ -25,7 +25,7 @@ type MockFn = jest.MockedFunction; describe('useSetupTechnology', () => { const updateNewAgentPolicyMock = jest.fn(); - const updateAgentPolicyMock = jest.fn(); + const updateAgentPoliciesMock = jest.fn(); const setSelectedPolicyTabMock = jest.fn(); const newAgentPolicyMock = { name: 'mock_new_agent_policy', @@ -59,7 +59,7 @@ describe('useSetupTechnology', () => { useSetupTechnology({ updateNewAgentPolicy: updateNewAgentPolicyMock, newAgentPolicy: newAgentPolicyMock, - updateAgentPolicy: updateAgentPolicyMock, + updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, }) ); @@ -74,7 +74,7 @@ describe('useSetupTechnology', () => { useSetupTechnology({ updateNewAgentPolicy: updateNewAgentPolicyMock, newAgentPolicy: newAgentPolicyMock, - updateAgentPolicy: updateAgentPolicyMock, + updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, }) ); @@ -95,7 +95,7 @@ describe('useSetupTechnology', () => { useSetupTechnology({ updateNewAgentPolicy: updateNewAgentPolicyMock, newAgentPolicy: newAgentPolicyMock, - updateAgentPolicy: updateAgentPolicyMock, + updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, }) ); @@ -110,7 +110,7 @@ describe('useSetupTechnology', () => { useSetupTechnology({ updateNewAgentPolicy: updateNewAgentPolicyMock, newAgentPolicy: newAgentPolicyMock, - updateAgentPolicy: updateAgentPolicyMock, + updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, }) ); @@ -121,7 +121,7 @@ describe('useSetupTechnology', () => { result.current.handleSetupTechnologyChange(SetupTechnology.AGENTLESS); }); - expect(updateAgentPolicyMock).toHaveBeenCalledWith({ id: 'agentless-policy-id' }); + expect(updateAgentPoliciesMock).toHaveBeenCalledWith([{ id: 'agentless-policy-id' }]); expect(setSelectedPolicyTabMock).toHaveBeenCalledWith(SelectedPolicyTab.EXISTING); }); @@ -130,7 +130,7 @@ describe('useSetupTechnology', () => { useSetupTechnology({ updateNewAgentPolicy: updateNewAgentPolicyMock, newAgentPolicy: newAgentPolicyMock, - updateAgentPolicy: updateAgentPolicyMock, + updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, }) ); @@ -164,7 +164,7 @@ describe('useSetupTechnology', () => { useSetupTechnology({ updateNewAgentPolicy: updateNewAgentPolicyMock, newAgentPolicy: newAgentPolicyMock, - updateAgentPolicy: updateAgentPolicyMock, + updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, }) ); @@ -183,7 +183,7 @@ describe('useSetupTechnology', () => { useSetupTechnology({ updateNewAgentPolicy: updateNewAgentPolicyMock, newAgentPolicy: newAgentPolicyMock, - updateAgentPolicy: updateAgentPolicyMock, + updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, }) ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts index ee2ef29f71d12f..cc67adddeca191 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts @@ -64,13 +64,13 @@ export const useAgentless = () => { export function useSetupTechnology({ updateNewAgentPolicy, newAgentPolicy, - updateAgentPolicy, + updateAgentPolicies, setSelectedPolicyTab, packageInfo, }: { updateNewAgentPolicy: (policy: NewAgentPolicy) => void; newAgentPolicy: NewAgentPolicy; - updateAgentPolicy: (policy: AgentPolicy | undefined) => void; + updateAgentPolicies: (policies: AgentPolicy[]) => void; setSelectedPolicyTab: (tab: SelectedPolicyTab) => void; packageInfo?: PackageInfo; }) { @@ -109,13 +109,13 @@ export function useSetupTechnology({ if (setupTechnology === SetupTechnology.AGENTLESS) { if (agentlessPolicy) { - updateAgentPolicy(agentlessPolicy); + updateAgentPolicies([agentlessPolicy]); setSelectedPolicyTab(SelectedPolicyTab.EXISTING); } } else if (setupTechnology === SetupTechnology.AGENT_BASED) { updateNewAgentPolicy(newAgentPolicy); setSelectedPolicyTab(SelectedPolicyTab.NEW); - updateAgentPolicy(undefined); + updateAgentPolicies([]); } setSelectedSetupTechnology(setupTechnology); }, @@ -123,7 +123,7 @@ export function useSetupTechnology({ isAgentlessEnabled, selectedSetupTechnology, agentlessPolicy, - updateAgentPolicy, + updateAgentPolicies, setSelectedPolicyTab, updateNewAgentPolicy, newAgentPolicy, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx index 43c2c4b4e28418..f79a5ad00c67a6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx @@ -60,6 +60,11 @@ jest.mock('../../../../hooks', () => { }, }, }), + sendBulkGetAgentPolicies: jest.fn().mockImplementation((ids) => + Promise.resolve({ + data: { items: ids.map((id: string) => ({ id, package_policies: [] })) }, + }) + ), useGetPackageInfoByKeyQuery: jest.fn(), sendGetSettings: jest.fn().mockResolvedValue({ data: { item: {} }, @@ -308,7 +313,6 @@ describe('When on the package policy create page', () => { title: 'Nginx', version: '1.3.0', }, - policy_id: '', policy_ids: ['agent-policy-1'], vars: undefined, }; @@ -371,6 +375,7 @@ describe('When on the package policy create page', () => { expect(sendCreatePackagePolicy as jest.MockedFunction).toHaveBeenCalledWith({ ...newPackagePolicy, policy_id: 'agent-policy-1', + policy_ids: ['agent-policy-1'], force: false, }); expect(sendCreateAgentPolicy as jest.MockedFunction).not.toHaveBeenCalled(); @@ -498,6 +503,9 @@ describe('When on the package policy create page', () => { (sendCreateAgentPolicy as jest.MockedFunction).mockClear(); (sendCreatePackagePolicy as jest.MockedFunction).mockClear(); + (sendGetAgentStatus as jest.MockedFunction).mockResolvedValue({ + data: { results: { total: 0 } }, + }); }); test('should create agent policy before creating package policy on submit when new hosts is selected', async () => { @@ -545,7 +553,7 @@ describe('When on the package policy create page', () => { }); test('should show modal if agent policy has agents', async () => { - (sendGetAgentStatus as jest.MockedFunction).mockResolvedValueOnce({ + (sendGetAgentStatus as jest.MockedFunction).mockResolvedValue({ data: { results: { total: 1 } }, }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx index 4bc608e4d90592..f2646152225621 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx @@ -155,8 +155,8 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ onSubmit, updatePackagePolicy, packagePolicy, - agentPolicy, - updateAgentPolicy, + agentPolicies, + updateAgentPolicies, savedPackagePolicy, formState, setFormState, @@ -216,19 +216,23 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ ); // Retrieve agent count - const agentPolicyId = agentPolicy?.id; + const agentPolicyIds = agentPolicies.map((policy) => policy.id); const { cancelClickHandler, cancelUrl } = useCancelAddPackagePolicy({ from, pkgkey: params.pkgkey, - agentPolicyId, + agentPolicyId: agentPolicyIds[0], }); useEffect(() => { const getAgentCount = async () => { - const { data } = await sendGetAgentStatus({ policyId: agentPolicyId }); - if (data?.results.total !== undefined) { - setAgentCount(data.results.total); + let count = 0; + for (const policyId of agentPolicyIds) { + const { data } = await sendGetAgentStatus({ policyId }); + if (data?.results.total) { + count += data.results.total; + } } + setAgentCount(count); }; if (selectedPolicyTab === SelectedPolicyTab.NEW) { @@ -236,10 +240,10 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ return; } - if (isFleetEnabled && agentPolicyId) { + if (isFleetEnabled && agentPolicyIds.length > 0) { getAgentCount(); } - }, [agentPolicyId, selectedPolicyTab, isFleetEnabled]); + }, [agentPolicyIds, selectedPolicyTab, isFleetEnabled]); const handleExtensionViewOnChange = useCallback< PackagePolicyEditExtensionComponentProps['onChange'] @@ -269,18 +273,18 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ from, cancelUrl, onCancel: cancelClickHandler, - agentPolicy, + agentPolicies, packageInfo, integrationInfo, }), - [agentPolicy, cancelClickHandler, cancelUrl, from, integrationInfo, packageInfo] + [agentPolicies, cancelClickHandler, cancelUrl, from, integrationInfo, packageInfo] ); const stepSelectAgentPolicy = useMemo( () => ( acc + (curr.unprivileged_agents ?? 0), + 0 + ); return ( - {formState === 'CONFIRM' && agentPolicy && ( + {formState === 'CONFIRM' && agentPolicies.length > 0 && ( setFormState('VALID')} showUnprivilegedAgentsCallout={Boolean( - packageInfo && - isRootPrivilegesRequired(packageInfo) && - (agentPolicy?.unprivileged_agents ?? 0) > 0 + packageInfo && isRootPrivilegesRequired(packageInfo) && unprivilegedAgentsCount > 0 )} - unprivilegedAgentsCount={agentPolicy?.unprivileged_agents ?? 0} + unprivilegedAgentsCount={unprivilegedAgentsCount} dataStreams={rootPrivilegedDataStreams} /> )} {formState === 'SUBMITTED_NO_AGENTS' && - agentPolicy && + agentPolicies.length > 0 && packageInfo && savedPackagePolicy && ( navigateAddAgentHelp(savedPackagePolicy)} /> )} - {formState === 'SUBMITTED_AZURE_ARM_TEMPLATE' && agentPolicy && savedPackagePolicy && ( - navigateAddAgent(savedPackagePolicy)} - onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} - /> - )} - {formState === 'SUBMITTED_CLOUD_FORMATION' && agentPolicy && savedPackagePolicy && ( - navigateAddAgent(savedPackagePolicy)} - onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} - /> - )} - {formState === 'SUBMITTED_GOOGLE_CLOUD_SHELL' && agentPolicy && savedPackagePolicy && ( - navigateAddAgent(savedPackagePolicy)} - onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} - /> - )} + {formState === 'SUBMITTED_AZURE_ARM_TEMPLATE' && + agentPolicies.length > 0 && + savedPackagePolicy && ( + navigateAddAgent(savedPackagePolicy)} + onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} + /> + )} + {formState === 'SUBMITTED_CLOUD_FORMATION' && + agentPolicies.length > 0 && + savedPackagePolicy && ( + navigateAddAgent(savedPackagePolicy)} + onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} + /> + )} + {formState === 'SUBMITTED_GOOGLE_CLOUD_SHELL' && + agentPolicies.length > 0 && + savedPackagePolicy && ( + navigateAddAgent(savedPackagePolicy)} + onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} + /> + )} {packageInfo && ( ( {agentCount ? ( { setAgentCount(0); submitUpdateAgentPolicy(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy.tsx index a44d218130d4f9..fe7d70d6976a93 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy.tsx @@ -81,7 +81,7 @@ export function usePackagePolicyWithRelatedData( }); const [originalPackagePolicy, setOriginalPackagePolicy] = useState(); - const [agentPolicy, setAgentPolicy] = useState(); + const [agentPolicies, setAgentPolicies] = useState([]); const [isLoadingData, setIsLoadingData] = useState(true); const [dryRunData, setDryRunData] = useState(); const [loadingError, setLoadingError] = useState(); @@ -171,17 +171,21 @@ export function usePackagePolicyWithRelatedData( throw packagePolicyError; } - const { data: agentPolicyData, error: agentPolicyError } = await sendGetOneAgentPolicy( - packagePolicyData!.item.policy_ids[0] // TODO multiple - ); + const newAgentPolicies = []; + for (const policyId of packagePolicyData!.item.policy_ids) { + const { data: agentPolicyData, error: agentPolicyError } = await sendGetOneAgentPolicy( + policyId + ); - if (agentPolicyError) { - throw agentPolicyError; - } + if (agentPolicyError) { + throw agentPolicyError; + } - if (agentPolicyData?.item) { - setAgentPolicy(agentPolicyData.item); + if (agentPolicyData?.item) { + newAgentPolicies.push(agentPolicyData.item); + } } + setAgentPolicies(newAgentPolicies); const { data: upgradePackagePolicyDryRunData, error: upgradePackagePolicyDryRunError } = await sendUpgradePackagePolicyDryRun([packagePolicyId]); @@ -353,7 +357,7 @@ export function usePackagePolicyWithRelatedData( isUpgrade, savePackagePolicy, isLoadingData, - agentPolicy, + agentPolicies, loadingError, packagePolicy, originalPackagePolicy, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index 475822e7789ffb..21d25ecdab2c62 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -6,7 +6,7 @@ */ import React, { useState, useEffect, useCallback, useMemo, memo } from 'react'; -import { omit } from 'lodash'; +import { isEmpty, omit } from 'lodash'; import { useRouteMatch } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -103,7 +103,7 @@ export const EditPackagePolicyForm = memo<{ const { // data - agentPolicy, + agentPolicies, isLoadingData, loadingError, packagePolicy, @@ -134,22 +134,26 @@ export const EditPackagePolicyForm = memo<{ return getNewSecrets({ packageInfo, packagePolicy }); }, [packageInfo, packagePolicy]); - const policyId = agentPolicy?.id ?? ''; + const policyIds = agentPolicies.map((policy) => policy.id); // Retrieve agent count const [agentCount, setAgentCount] = useState(0); useEffect(() => { const getAgentCount = async () => { - const { data } = await sendGetAgentStatus({ policyId }); - if (data?.results.total) { - setAgentCount(data.results.total); + let count = 0; + for (const policyId of policyIds) { + const { data } = await sendGetAgentStatus({ policyId }); + if (data?.results.total) { + count += data.results.total; + } } + setAgentCount(count); }; - if (isFleetEnabled && policyId) { + if (isFleetEnabled && policyIds.length > 0) { getAgentCount(); } - }, [policyId, isFleetEnabled]); + }, [policyIds, isFleetEnabled]); const handleExtensionViewOnChange = useCallback< PackagePolicyEditExtensionComponentProps['onChange'] @@ -170,25 +174,25 @@ 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 && policyId) { + if (packageInfo && policyIds.length > 0) { return from === 'package-edit' ? getHref('integration_details_policies', { pkgkey: pkgKeyFromPackageInfo(packageInfo!), }) - : getHref('policy_details', { policyId }); + : getHref('policy_details', { policyId: policyIds[0] }); } return '/'; - }, [from, getHref, packageInfo, policyId]); + }, [from, getHref, packageInfo, policyIds]); const successRedirectPath = useMemo(() => { - if (packageInfo && policyId) { + if (packageInfo && policyIds.length > 0) { return from === 'package-edit' || from === 'upgrade-from-integrations-policy-list' ? getHref('integration_details_policies', { pkgkey: pkgKeyFromPackageInfo(packageInfo!), }) - : getHref('policy_details', { policyId }); + : getHref('policy_details', { policyId: policyIds[0] }); } return '/'; - }, [from, getHref, packageInfo, policyId]); + }, [from, getHref, packageInfo, policyIds]); useHistoryBlock(isEdited); @@ -197,7 +201,7 @@ export const EditPackagePolicyForm = memo<{ setFormState('INVALID'); return; } - if (agentCount !== 0 && policyId !== AGENTLESS_POLICY_ID && formState !== 'CONFIRM') { + if (agentCount !== 0 && !policyIds.includes(AGENTLESS_POLICY_ID) && formState !== 'CONFIRM') { setFormState('CONFIRM'); return; } @@ -215,11 +219,11 @@ export const EditPackagePolicyForm = memo<{ }), 'data-test-subj': 'policyUpdateSuccessToast', text: - agentCount && agentPolicy + agentCount && agentPolicies.length > 0 ? i18n.translate('xpack.fleet.editPackagePolicy.updatedNotificationMessage', { - defaultMessage: `Fleet will deploy updates to all agents that use the ''{agentPolicyName}'' policy`, + defaultMessage: `Fleet will deploy updates to all agents that use the ''{agentPolicyNames}'' policy`, values: { - agentPolicyName: agentPolicy.name, + agentPolicyNames: agentPolicies.map((policy) => policy.name).join(', '), }, }) : undefined, @@ -276,7 +280,7 @@ export const EditPackagePolicyForm = memo<{ const layoutProps = { from: extensionView?.useLatestPackageVersion && isUpgrade ? 'upgrade-from-extension' : from, cancelUrl, - agentPolicy, + agentPolicies, packageInfo, tabs: tabsViews?.length ? [ @@ -302,11 +306,11 @@ export const EditPackagePolicyForm = memo<{ const configurePackage = useMemo( () => - agentPolicy && packageInfo ? ( + agentPolicies && packageInfo ? ( <> {selectedTab === 0 && ( ) : null, [ - agentPolicy, + agentPolicies, packageInfo, packagePolicy, updatePackagePolicy, @@ -368,7 +372,7 @@ export const EditPackagePolicyForm = memo<{ const replaceConfigurePackage = replaceDefineStepView && originalPackagePolicy && packageInfo && ( {isLoadingData ? ( - ) : loadingError || !agentPolicy || !packageInfo ? ( + ) : loadingError || isEmpty(agentPolicies) || !packageInfo ? ( {formState === 'CONFIRM' && ( setFormState('VALID')} /> @@ -453,7 +457,7 @@ export const EditPackagePolicyForm = memo<{ - {agentPolicy && packageInfo && formState === 'INVALID' ? ( + {agentPolicies && packageInfo && formState === 'INVALID' ? ( => { if (!data?.items) { return []; } - const newPolicies = data.items.map(({ agentPolicy, packagePolicy }) => { + const newPolicies = data.items.map(({ agentPolicies, packagePolicy }) => { const hasUpgrade = isPackagePolicyUpgradable(packagePolicy); return { - agentPolicy, + agentPolicies, packagePolicy: { ...packagePolicy, hasUpgrade, @@ -140,8 +140,8 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps return newPolicies; }, [data?.items, isPackagePolicyUpgradable]); - const showAddAgentHelpForPackagePolicyId = packageAndAgentPolicies.find( - ({ agentPolicy }) => agentPolicy?.id === showAddAgentHelpForPolicyId + const showAddAgentHelpForPackagePolicyId = packageAndAgentPolicies.find(({ agentPolicies }) => + agentPolicies.find((agentPolicy) => agentPolicy.id === showAddAgentHelpForPolicyId) )?.packagePolicy?.id; // Handle the "add agent" link displayed in post-installation toast notifications in the case // where a user is clicking the link while on the package policies listing page @@ -184,7 +184,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.version', { defaultMessage: 'Version', }), - render(_version, { agentPolicy, packagePolicy }) { + render(_version, { agentPolicies, packagePolicy }) { return ( @@ -197,13 +197,13 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps - {agentPolicy && packagePolicy.hasUpgrade && ( + {agentPolicies.length > 0 && packagePolicy.hasUpgrade && ( + render(id, { agentPolicies }) { + return agentPolicies.length > 0 ? ( + // TODO: handle multiple agent policies + ) : ( ); @@ -263,10 +264,11 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentCount', { defaultMessage: 'Agents', }), - render({ agentPolicy, packagePolicy }: InMemoryPackagePolicyAndAgentPolicy) { - if (!agentPolicy) { + render({ agentPolicies, packagePolicy }: InMemoryPackagePolicyAndAgentPolicy) { + if (agentPolicies.length === 0) { return null; } + const agentPolicy = agentPolicies[0]; // TODO: handle multiple agent policies const canAddAgentsForPolicy = policyHasFleetServer(agentPolicy) ? canAddFleetServers : canAddAgents; @@ -288,7 +290,8 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps }), width: '8ch', align: 'right', - render({ agentPolicy, packagePolicy }) { + render({ agentPolicies, packagePolicy }) { + const agentPolicy = agentPolicies[0]; // TODO: handle multiple agent policies return ( ); } - const selectedPolicies = packageAndAgentPolicies.find( - ({ agentPolicy: policy }) => policy?.id === flyoutOpenForPolicyId + const selectedPolicies = packageAndAgentPolicies.find(({ agentPolicies: policies }) => + policies.find((policy) => policy.id === flyoutOpenForPolicyId) ); - const agentPolicy = selectedPolicies?.agentPolicy; + const agentPolicies = selectedPolicies?.agentPolicies; const packagePolicy = selectedPolicies?.packagePolicy; return ( @@ -364,14 +367,14 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps /> - {flyoutOpenForPolicyId && agentPolicy && !isLoading && ( + {flyoutOpenForPolicyId && agentPolicies && !isLoading && ( { setFlyoutOpenForPolicyId(null); const { addAgentToPolicyId, ...rest } = parse(search); history.replace({ search: stringify(rest) }); }} - agentPolicy={agentPolicy} + agentPolicy={agentPolicies[0]} isIntegrationFlow={true} installedPackagePolicy={{ name: packagePolicy?.package?.name || '', diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts index 560a2900ae406f..f975933d53d99b 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts @@ -19,13 +19,9 @@ import { agentPolicyRouteService } from '../../../../../services'; import { useGetPackagePolicies, useConditionalRequest } from '../../../../../hooks'; import type { SendConditionalRequestConfig } from '../../../../../hooks'; -export interface PackagePolicyEnriched extends PackagePolicy { - _agentPolicy: GetAgentPoliciesResponseItem | undefined; -} - export interface PackagePolicyAndAgentPolicy { packagePolicy: PackagePolicy; - agentPolicy: GetAgentPoliciesResponseItem; + agentPolicies: GetAgentPoliciesResponseItem[]; } type GetPackagePoliciesWithAgentPolicy = Omit & { @@ -34,7 +30,7 @@ type GetPackagePoliciesWithAgentPolicy = Omit { return { packagePolicy, - agentPolicy: agentPoliciesById[packagePolicy.policy_ids[0]], // TODO multiple agent policies + agentPolicies: packagePolicy.policy_ids + .map((policyId: string) => agentPoliciesById[policyId]) + .filter((policy) => !!policy), }; } ); diff --git a/x-pack/plugins/fleet/public/hooks/use_request/agent_policy.ts b/x-pack/plugins/fleet/public/hooks/use_request/agent_policy.ts index cbccc54273e298..837222dae103fa 100644 --- a/x-pack/plugins/fleet/public/hooks/use_request/agent_policy.ts +++ b/x-pack/plugins/fleet/public/hooks/use_request/agent_policy.ts @@ -65,6 +65,15 @@ export const useBulkGetAgentPoliciesQuery = (ids: string[], options?: { full?: b ); }; +export const sendBulkGetAgentPolicies = (ids: string[], options?: { full?: boolean }) => { + return sendRequest({ + path: agentPolicyRouteService.getBulkGetPath(), + method: 'post', + body: JSON.stringify({ ids, full: options?.full }), + version: API_VERSIONS.public.v1, + }); +}; + export const sendGetAgentPolicies = (query?: GetAgentPoliciesRequest['query']) => { return sendRequest({ path: agentPolicyRouteService.getListPath(),