From e374530da0b2674a78aac6768e0768d87cb6d887 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 19 Jan 2026 10:44:57 -0500 Subject: [PATCH 1/9] [Fleet] Add Copy integration policy feature --- .../shared/fleet/common/constants/epm.ts | 2 + .../copy_package_policy_page/index.tsx | 57 +++++++++++++++++++ .../single_page_layout/components/layout.tsx | 35 +++++++++++- .../single_page_layout/hooks/form.tsx | 21 +++++++ .../single_page_layout/index.tsx | 26 ++++++++- .../create_package_policy_page/types.ts | 6 +- .../fleet/sections/agent_policy/index.tsx | 4 ++ .../package_policy_actions_menu.test.tsx | 32 +++++++++++ .../package_policy_actions_menu.tsx | 25 +++++--- .../fleet/public/constants/page_paths.ts | 6 ++ .../public/hooks/use_request/agent_policy.ts | 21 ++++--- 11 files changed, 215 insertions(+), 20 deletions(-) create mode 100644 x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx diff --git a/x-pack/platform/plugins/shared/fleet/common/constants/epm.ts b/x-pack/platform/plugins/shared/fleet/common/constants/epm.ts index f9dbe5a0eab22..769fb3a22535a 100644 --- a/x-pack/platform/plugins/shared/fleet/common/constants/epm.ts +++ b/x-pack/platform/plugins/shared/fleet/common/constants/epm.ts @@ -87,6 +87,8 @@ export const HIDDEN_API_REFERENCE_PACKAGES = [ FLEET_SYNTHETICS_PACKAGE, ]; +export const EXCLUDED_FROM_PACKAGE_POLICY_COPY_PACKAGES = [FLEET_ENDPOINT_PACKAGE]; + export const autoUpgradePoliciesPackages = [ FLEET_APM_PACKAGE, FLEET_SYNTHETICS_PACKAGE, diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx new file mode 100644 index 0000000000000..d389266e8e5c6 --- /dev/null +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, memo } from 'react'; +import { useRouteMatch, useLocation } from 'react-router-dom'; + +import { useBulkGetAgentPoliciesQuery } from '../../../hooks'; +import { useGetOnePackagePolicy } from '../../../../integrations/hooks'; +import { Loading } from '../../../components'; +import type { EditPackagePolicyFrom } from '../create_package_policy_page/types'; + +import { CreatePackagePolicySinglePage } from '../create_package_policy_page/single_page_layout'; + +export const CopyPackagePolicyPage = memo(() => { + const { + params: { packagePolicyId }, + } = useRouteMatch<{ policyId: string; packagePolicyId: string }>(); + + const packagePolicy = useGetOnePackagePolicy(packagePolicyId); + + const packagePolicyData = useMemo(() => { + if (packagePolicy.data?.item) { + return { + ...packagePolicy.data.item, + name: 'copy-' + packagePolicy.data.item.name, + }; + } + }, [packagePolicy.data?.item]); + + const agentPolicies = useBulkGetAgentPoliciesQuery(packagePolicyData?.policy_ids || [], { + enabled: !!packagePolicyData?.policy_ids, + }); + + // Parse the 'from' query parameter to determine navigation after save + const { search } = useLocation(); + const qs = new URLSearchParams(search); + const fromQs = qs.get('from') as EditPackagePolicyFrom | null; + + if (packagePolicy.isLoading || agentPolicies.isLoading || !agentPolicies.data) { + return ; + } + + return ( + + ); +}); diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/layout.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/layout.tsx index 94d38ac74a569..ba58a932ac314 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/layout.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/layout.tsx @@ -22,7 +22,12 @@ import { import { useAgentless } from '../hooks/setup_technology'; import { WithHeaderLayout } from '../../../../../layouts'; -import type { AgentPolicy, PackageInfo, RegistryPolicyTemplate } from '../../../../../types'; +import type { + AgentPolicy, + PackageInfo, + PackagePolicy, + RegistryPolicyTemplate, +} from '../../../../../types'; import { PackageIcon } from '../../../../../components'; import type { EditPackagePolicyFrom } from '../../types'; @@ -41,6 +46,7 @@ export const CreatePackagePolicySinglePageLayout: React.FunctionComponent<{ agentPolicy?: AgentPolicy; packageInfo?: PackageInfo; integrationInfo?: RegistryPolicyTemplate; + defaultPolicyData?: PackagePolicy; 'data-test-subj'?: string; tabs?: Array<{ title: string; @@ -57,6 +63,7 @@ export const CreatePackagePolicySinglePageLayout: React.FunctionComponent<{ packageInfo, integrationInfo, children, + defaultPolicyData, 'data-test-subj': dataTestSubj, tabs = [], }) => { @@ -72,6 +79,16 @@ export const CreatePackagePolicySinglePageLayout: React.FunctionComponent<{ [from] ); + const isCopy = useMemo( + () => + [ + 'copy-from-fleet-policy-list', + 'copy-from-integrations-policy-list', + 'copy-from-extension', + ].includes(from), + [from] + ); + const pageTitle = useMemo(() => { if ((isAdd || isEdit || isUpgrade) && packageInfo) { let pageTitleText = ( @@ -152,6 +169,22 @@ export const CreatePackagePolicySinglePageLayout: React.FunctionComponent<{ ); } + if (isCopy) { + return ( + +

+ +

+
+ ); + } + return (

diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx index 3e7419bf502af..b058b8c7a39d5 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx @@ -284,6 +284,7 @@ export function useOnSubmit({ setNewAgentPolicy, setSelectedPolicyTab, isAddIntegrationFlyout, + defaultPolicyData, }: { packageInfo?: PackageInfo; newAgentPolicy: NewAgentPolicy; @@ -296,6 +297,7 @@ export function useOnSubmit({ setNewAgentPolicy: (policy: NewAgentPolicy) => void; setSelectedPolicyTab: (tab: SelectedPolicyTab) => void; isAddIntegrationFlyout?: boolean; + defaultPolicyData?: Partial; }) { const { notifications, docLinks } = useStartServices(); const { spaceId } = useFleetStatus(); @@ -419,6 +421,23 @@ export function useOnSubmit({ integrationToEnable ); + if (defaultPolicyData) { + basePackagePolicy.name = defaultPolicyData.name || basePackagePolicy.name; + basePackagePolicy.description = + defaultPolicyData.description || basePackagePolicy.description; + basePackagePolicy.namespace = defaultPolicyData.namespace || basePackagePolicy.namespace; + if (defaultPolicyData.inputs) { + basePackagePolicy.inputs = defaultPolicyData.inputs; + } + if (defaultPolicyData.vars) { + basePackagePolicy.vars = defaultPolicyData.vars; + } + + if (defaultPolicyData.policy_ids) { + basePackagePolicy.policy_ids = defaultPolicyData.policy_ids; + } + } + // Set the package policy with the fetched package updatePackagePolicy(basePackagePolicy); setIsInitialized(true); @@ -440,6 +459,8 @@ export function useOnSubmit({ integration, setIntegration, isAddIntegrationFlyout, + defaultPolicyData, + setSelectedPolicyTab, ]); useEffect(() => { diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx index d3e9f1d8b61c3..ad6b4a31757ac 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx @@ -115,6 +115,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ integration, pkgLabel, addIntegrationFlyoutProps, + defaultPolicyData, }) => { const { agents: { enabled: isFleetEnabled }, @@ -144,7 +145,9 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ }); const [selectedPolicyTab, setSelectedPolicyTab] = useState( - queryParamsPolicyId ? SelectedPolicyTab.EXISTING : SelectedPolicyTab.NEW + queryParamsPolicyId || (defaultPolicyData?.policy_ids?.length ?? 0) > 0 + ? SelectedPolicyTab.EXISTING + : SelectedPolicyTab.NEW ); const { @@ -217,6 +220,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ setNewAgentPolicy, setSelectedPolicyTab, isAddIntegrationFlyout, + defaultPolicyData, }); if (addIntegrationFlyoutProps?.agentPolicy) { @@ -361,8 +365,17 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ agentPolicies, packageInfo, integrationInfo, + defaultPolicyData, }), - [agentPolicies, cancelClickHandler, cancelUrl, from, integrationInfo, packageInfo] + [ + agentPolicies, + cancelClickHandler, + cancelUrl, + from, + integrationInfo, + packageInfo, + defaultPolicyData, + ] ); const stepSelectAgentPolicy = useMemo( @@ -378,7 +391,13 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ packageInfo={packageInfo} setHasAgentPolicyError={setHasAgentPolicyError} updateSelectedTab={updateSelectedPolicyTab} - selectedAgentPolicyIds={queryParamsPolicyId ? [queryParamsPolicyId] : []} + selectedAgentPolicyIds={ + queryParamsPolicyId + ? [queryParamsPolicyId] + : defaultPolicyData?.policy_ids + ? defaultPolicyData?.policy_ids + : [] + } /> ), [ @@ -392,6 +411,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ updateSelectedPolicyTab, queryParamsPolicyId, setHasAgentPolicyError, + defaultPolicyData, ] ); diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts index ec1ec42685492..7ab38ebdf9f3d 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts @@ -9,7 +9,7 @@ import type { EuiStepProps } from '@elastic/eui'; import type React from 'react'; -import type { AgentPolicy } from '../../../../../../common'; +import type { AgentPolicy, PackagePolicy } from '../../../../../../common'; export type EditPackagePolicyFrom = | 'package' | 'package-edit' @@ -18,6 +18,8 @@ export type EditPackagePolicyFrom = | 'upgrade-from-fleet-policy-list' | 'upgrade-from-integrations-policy-list' | 'upgrade-from-extension' + | 'copy-from-fleet-policy-list' + | 'copy-from-integrations-policy-list' | 'installed-integrations'; export type PackagePolicyFormState = @@ -45,6 +47,8 @@ export type CreatePackagePolicyParams = React.FunctionComponent<{ pkgVersion?: string; integration?: string; pkgLabel?: string; + defaultPolicyData?: Partial; + defaultAgentPolicies?: AgentPolicy[]; addIntegrationFlyoutProps?: { selectIntegrationStep: EuiStepProps; onSubmitCompleted: () => void; diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/index.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/index.tsx index abce53e850d4c..2c1dfdeb2a56e 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/index.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/index.tsx @@ -17,6 +17,7 @@ import { AgentPolicyListPage } from './list_page'; import { AgentPolicyDetailsPage } from './details_page'; import { EditPackagePolicyPage } from './edit_package_policy_page'; import { UpgradePackagePolicyPage } from './upgrade_package_policy_page'; +import { CopyPackagePolicyPage } from './copy_package_policy_page'; export const AgentPolicyApp: React.FunctionComponent = () => { useBreadcrumbs('policies'); @@ -26,6 +27,9 @@ export const AgentPolicyApp: React.FunctionComponent = () => { + + + diff --git a/x-pack/platform/plugins/shared/fleet/public/components/package_policy_actions_menu.test.tsx b/x-pack/platform/plugins/shared/fleet/public/components/package_policy_actions_menu.test.tsx index 7f2c5e1a638db..e5c0aa9da17f4 100644 --- a/x-pack/platform/plugins/shared/fleet/public/components/package_policy_actions_menu.test.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/components/package_policy_actions_menu.test.tsx @@ -254,4 +254,36 @@ describe('PackagePolicyActionsMenu', () => { }); }); }); + + it('Should disable Copy integration for excluded packages', async () => { + const agentPolicies = createMockAgentPolicies(); + const packagePolicy = createMockPackagePolicy({ + package: { + name: 'endpoint', + version: '1.0.0', + title: 'Elastic Defend', + }, + }); + const { utils } = renderMenu({ agentPolicies, packagePolicy }); + await waitFor(() => { + const copyButton = utils.getByTestId('PackagePolicyActionsCopyItem'); + expect(copyButton).toBeDisabled(); + }); + }); + + it('Should enable Copy integration for non-excluded packages', async () => { + const agentPolicies = createMockAgentPolicies(); + const packagePolicy = createMockPackagePolicy({ + package: { + name: 'some-other-package', + version: '1.0.0', + title: 'Some Other Package', + }, + }); + const { utils } = renderMenu({ agentPolicies, packagePolicy }); + await waitFor(() => { + const copyButton = utils.getByTestId('PackagePolicyActionsCopyItem'); + expect(copyButton).not.toBeDisabled(); + }); + }); }); diff --git a/x-pack/platform/plugins/shared/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/platform/plugins/shared/fleet/public/components/package_policy_actions_menu.tsx index f17c0f82d40dc..f2721b2838e2b 100644 --- a/x-pack/platform/plugins/shared/fleet/public/components/package_policy_actions_menu.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/components/package_policy_actions_menu.tsx @@ -9,6 +9,7 @@ import React, { useMemo, useState } from 'react'; import { EuiContextMenuItem, EuiPortal } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { EXCLUDED_FROM_PACKAGE_POLICY_COPY_PACKAGES } from '../../common/constants'; import type { AgentPolicy, InMemoryPackagePolicy } from '../types'; import { useAgentPolicyRefresh, useAuthz, useLink } from '../hooks'; import { policyHasFleetServer } from '../services'; @@ -126,13 +127,23 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ , ] : []), - // FIXME: implement Copy package policy action - // {}} key="packagePolicyCopy"> - // - // , + + + , ]; if (!agentPolicy || !agentPolicyIsManaged || agentPolicy?.supports_agentless) { diff --git a/x-pack/platform/plugins/shared/fleet/public/constants/page_paths.ts b/x-pack/platform/plugins/shared/fleet/public/constants/page_paths.ts index 51f948d07d674..99071fb1b442f 100644 --- a/x-pack/platform/plugins/shared/fleet/public/constants/page_paths.ts +++ b/x-pack/platform/plugins/shared/fleet/public/constants/page_paths.ts @@ -42,6 +42,7 @@ export type DynamicPage = | 'policy_details' | 'add_integration_to_policy' | 'edit_integration' + | 'copy_integration' | 'upgrade_package_policy' | 'agent_list' | 'agent_details' @@ -76,6 +77,7 @@ export const FLEET_ROUTING_PATHS = { policy_details: '/policies/:policyId/:tabId?', policy_details_settings: '/policies/:policyId/settings', edit_integration: '/policies/:policyId/edit-integration/:packagePolicyId', + copy_integration: '/policies/copy-integration/:packagePolicyId', upgrade_package_policy: '/policies/:policyId/upgrade-package-policy/:packagePolicyId', enrollment_tokens: '/enrollment-tokens', uninstall_tokens: '/uninstall-tokens', @@ -253,6 +255,10 @@ export const pagePathGetters: { FLEET_BASE_PATH, `/policies/${policyId}/edit-integration/${packagePolicyId}`, ], + copy_integration: ({ packagePolicyId }) => [ + FLEET_BASE_PATH, + `/policies/copy-integration/${packagePolicyId}`, + ], upgrade_package_policy: ({ policyId, packagePolicyId }) => [ FLEET_BASE_PATH, `/policies/${policyId}/upgrade-package-policy/${packagePolicyId}`, diff --git a/x-pack/platform/plugins/shared/fleet/public/hooks/use_request/agent_policy.ts b/x-pack/platform/plugins/shared/fleet/public/hooks/use_request/agent_policy.ts index 98cd260fdc613..4195543cd20cd 100644 --- a/x-pack/platform/plugins/shared/fleet/public/hooks/use_request/agent_policy.ts +++ b/x-pack/platform/plugins/shared/fleet/public/hooks/use_request/agent_policy.ts @@ -63,15 +63,20 @@ export const useGetAgentPoliciesQuery = ( export const useBulkGetAgentPoliciesQuery = ( ids: string[], - options?: { full?: boolean; ignoreMissing?: boolean } + options?: { full?: boolean; ignoreMissing?: boolean; enabled?: boolean } ) => { - return useQuery(['agentPolicies', ids], () => - sendRequestForRq({ - path: agentPolicyRouteService.getBulkGetPath(), - method: 'post', - body: JSON.stringify({ ids, full: options?.full }), - version: API_VERSIONS.public.v1, - }) + return useQuery( + ['agentPolicies', ids], + () => + sendRequestForRq({ + path: agentPolicyRouteService.getBulkGetPath(), + method: 'post', + body: JSON.stringify({ ids, full: options?.full }), + version: API_VERSIONS.public.v1, + }), + { + enabled: options?.enabled, + } ); }; From b8c5ec81e81fd956d1d5a5c6962625d226e9fee6 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 19 Jan 2026 13:17:49 -0500 Subject: [PATCH 2/9] fix linting --- .../agent_policy/copy_package_policy_page/index.tsx | 10 ++-------- .../single_page_layout/components/layout.tsx | 6 ++++-- .../agent_policy/create_package_policy_page/types.ts | 1 - 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx index d389266e8e5c6..0c56b067b080b 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx @@ -8,7 +8,6 @@ import React, { useMemo, memo } from 'react'; import { useRouteMatch, useLocation } from 'react-router-dom'; -import { useBulkGetAgentPoliciesQuery } from '../../../hooks'; import { useGetOnePackagePolicy } from '../../../../integrations/hooks'; import { Loading } from '../../../components'; import type { EditPackagePolicyFrom } from '../create_package_policy_page/types'; @@ -18,7 +17,7 @@ import { CreatePackagePolicySinglePage } from '../create_package_policy_page/sin export const CopyPackagePolicyPage = memo(() => { const { params: { packagePolicyId }, - } = useRouteMatch<{ policyId: string; packagePolicyId: string }>(); + } = useRouteMatch<{ packagePolicyId: string }>(); const packagePolicy = useGetOnePackagePolicy(packagePolicyId); @@ -31,16 +30,12 @@ export const CopyPackagePolicyPage = memo(() => { } }, [packagePolicy.data?.item]); - const agentPolicies = useBulkGetAgentPoliciesQuery(packagePolicyData?.policy_ids || [], { - enabled: !!packagePolicyData?.policy_ids, - }); - // Parse the 'from' query parameter to determine navigation after save const { search } = useLocation(); const qs = new URLSearchParams(search); const fromQs = qs.get('from') as EditPackagePolicyFrom | null; - if (packagePolicy.isLoading || agentPolicies.isLoading || !agentPolicies.data) { + if (packagePolicy.isLoading) { return ; } @@ -50,7 +45,6 @@ export const CopyPackagePolicyPage = memo(() => { pkgName={packagePolicy.data!.item!.package!.name} pkgVersion={packagePolicy.data!.item!.package!.version} defaultPolicyData={packagePolicyData} - defaultAgentPolicies={agentPolicies.data?.items || []} prerelease={true} /> ); diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/layout.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/layout.tsx index ba58a932ac314..2c92829dfc685 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/layout.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/layout.tsx @@ -46,7 +46,7 @@ export const CreatePackagePolicySinglePageLayout: React.FunctionComponent<{ agentPolicy?: AgentPolicy; packageInfo?: PackageInfo; integrationInfo?: RegistryPolicyTemplate; - defaultPolicyData?: PackagePolicy; + defaultPolicyData?: Partial; 'data-test-subj'?: string; tabs?: Array<{ title: string; @@ -177,7 +177,7 @@ export const CreatePackagePolicySinglePageLayout: React.FunctionComponent<{ id="xpack.fleet.copyPackagePolicy.pageTitle" defaultMessage="Create integration from {packagePolicyName}" values={{ - packagePolicyName: defaultPolicyData?.name.replace(/^copy-/, '') || '', + packagePolicyName: defaultPolicyData?.name?.replace(/^copy-/, '') || '', }} />

@@ -201,9 +201,11 @@ export const CreatePackagePolicySinglePageLayout: React.FunctionComponent<{ integrationInfo?.name, integrationInfo?.title, packageInfo, + defaultPolicyData?.name, isAdd, isEdit, isUpgrade, + isCopy, ]); const pageDescription = useMemo(() => { diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts index 7ab38ebdf9f3d..eba021657ee6c 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts @@ -48,7 +48,6 @@ export type CreatePackagePolicyParams = React.FunctionComponent<{ integration?: string; pkgLabel?: string; defaultPolicyData?: Partial; - defaultAgentPolicies?: AgentPolicy[]; addIntegrationFlyoutProps?: { selectIntegrationStep: EuiStepProps; onSubmitCompleted: () => void; From 1829fe076720fb8d58f14d417371238ea9395e0e Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 19 Jan 2026 13:37:25 -0500 Subject: [PATCH 3/9] fix TODO --- .../single_page_layout/hooks/form.tsx | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx index b058b8c7a39d5..f79138a4a05b5 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { load } from 'js-yaml'; -import { isEqual, omit } from 'lodash'; +import { isEqual, omit, pick } from 'lodash'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiLink } from '@elastic/eui'; @@ -422,20 +422,26 @@ export function useOnSubmit({ ); if (defaultPolicyData) { - basePackagePolicy.name = defaultPolicyData.name || basePackagePolicy.name; - basePackagePolicy.description = - defaultPolicyData.description || basePackagePolicy.description; - basePackagePolicy.namespace = defaultPolicyData.namespace || basePackagePolicy.namespace; - if (defaultPolicyData.inputs) { - basePackagePolicy.inputs = defaultPolicyData.inputs; - } - if (defaultPolicyData.vars) { - basePackagePolicy.vars = defaultPolicyData.vars; - } - - if (defaultPolicyData.policy_ids) { - basePackagePolicy.policy_ids = defaultPolicyData.policy_ids; - } + Object.assign( + basePackagePolicy, + pick( + defaultPolicyData, + 'name', + 'description', + 'namespace', + 'policy_ids', + 'output_id', + 'cloud_connector_id', + 'cloud_connector_name', + 'inputs', + 'vars', + 'elasticsearch', + 'overrides', + 'supports_agentless', + 'supports_cloud_connector', + 'additional_datastreams_permissions' + ) + ); } // Set the package policy with the fetched package From ac87fbd910e5648e19217883d6d016f0c052dcea Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 19 Jan 2026 15:03:57 -0500 Subject: [PATCH 4/9] Add tests --- .../package_policy_input_stream.tsx | 5 +- .../page_objects/copy_integration_page.ts | 64 ++++++++ .../scout/ui/fixtures/page_objects/index.ts | 3 + .../scout/ui/tests/copy_integration.spec.ts | 154 ++++++++++++++++++ 4 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 x-pack/platform/plugins/shared/fleet/test/scout/ui/fixtures/page_objects/copy_integration_page.ts create mode 100644 x-pack/platform/plugins/shared/fleet/test/scout/ui/tests/copy_integration.spec.ts diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx index 160c888eaa691..5fbfa7f6a5087 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx @@ -173,7 +173,10 @@ export const PackagePolicyInputStreamConfig = memo( return ( <> - + diff --git a/x-pack/platform/plugins/shared/fleet/test/scout/ui/fixtures/page_objects/copy_integration_page.ts b/x-pack/platform/plugins/shared/fleet/test/scout/ui/fixtures/page_objects/copy_integration_page.ts new file mode 100644 index 0000000000000..4381bd8c1ffc7 --- /dev/null +++ b/x-pack/platform/plugins/shared/fleet/test/scout/ui/fixtures/page_objects/copy_integration_page.ts @@ -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 ScoutPage } from '@kbn/scout'; + +export class CopyIntegrationPage { + constructor(private readonly page: ScoutPage) {} + + async navigateTo(packagePolicyId: string) { + await this.page.gotoApp(`fleet/policies/copy-integration/${packagePolicyId}`); + } + + async waitForPageToLoad() { + await this.page.waitForLoadingIndicatorHidden(); + } + + getPackagePolicyNameInput() { + return this.page.testSubj.locator('packagePolicyNameInput'); + } + + getSaveButton() { + return this.page.testSubj.locator('createPackagePolicySaveButton'); + } + + getAgentPolicySelect() { + return this.page.testSubj.locator('agentPolicyMultiSelect'); + } + + async fillPackagePolicyName(name: string) { + const input = this.getPackagePolicyNameInput(); + await input.clear(); + await input.fill(name); + } + + /** + * Get the multi-text input for a variable by its field name. + * The selector is based on the field label lowercased with spaces replaced by dashes. + */ + getMultiTextInput(fieldName: string) { + return this.page.testSubj.locator(`multiTextInput-${fieldName}`); + } + + /** + * Get the first row of a multi-text input (the actual input element) + */ + getMultiTextInputRow(dataset: string, fieldName: string, index: number = 0) { + return this.page.testSubj + .locator(`streamOptions.inputStreams.${dataset}`) + .getByTestId(`multiTextInput-${fieldName}`) + .getByTestId(`multiTextInputRow-${index}`); + } + + getSuccessPostInstallAddAgentModal() { + return this.page.testSubj.locator('postInstallAddAgentModal'); + } + + async clickSaveButton() { + await this.getSaveButton().click(); + } +} diff --git a/x-pack/platform/plugins/shared/fleet/test/scout/ui/fixtures/page_objects/index.ts b/x-pack/platform/plugins/shared/fleet/test/scout/ui/fixtures/page_objects/index.ts index 4b0246e616539..f103c2d6531a7 100644 --- a/x-pack/platform/plugins/shared/fleet/test/scout/ui/fixtures/page_objects/index.ts +++ b/x-pack/platform/plugins/shared/fleet/test/scout/ui/fixtures/page_objects/index.ts @@ -9,14 +9,17 @@ import type { PageObjects, ScoutPage } from '@kbn/scout'; import { createLazyPageObject } from '@kbn/scout'; import { BrowseIntegrationPage } from './browse_integrations_page'; +import { CopyIntegrationPage } from './copy_integration_page'; export interface StreamsPageObjects extends PageObjects { browseIntegrations: BrowseIntegrationPage; + copyIntegration: CopyIntegrationPage; } export function extendPageObjects(pageObjects: PageObjects, page: ScoutPage): StreamsPageObjects { return { ...pageObjects, browseIntegrations: createLazyPageObject(BrowseIntegrationPage, page), + copyIntegration: createLazyPageObject(CopyIntegrationPage, page), }; } diff --git a/x-pack/platform/plugins/shared/fleet/test/scout/ui/tests/copy_integration.spec.ts b/x-pack/platform/plugins/shared/fleet/test/scout/ui/tests/copy_integration.spec.ts new file mode 100644 index 0000000000000..e006a7f586137 --- /dev/null +++ b/x-pack/platform/plugins/shared/fleet/test/scout/ui/tests/copy_integration.spec.ts @@ -0,0 +1,154 @@ +/* + * 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 { expect } from '@kbn/scout'; + +import { test } from '../fixtures'; + +test.describe('Copy integration', { tag: ['@ess'] }, () => { + const testAgentPolicyName = 'Test Agent Policy for Copy'; + const packagePolicyName = 'nginx-test-copy'; + let agentPolicyId: string; + let packagePolicyId: string; + + test.beforeAll(async ({ kbnClient }) => { + const agentPolicyResponse = await kbnClient.request<{ item: { id: string } }>({ + method: 'POST', + path: '/api/fleet/agent_policies', + body: { + name: testAgentPolicyName, + namespace: 'default', + monitoring_enabled: ['logs', 'metrics'], + }, + }); + agentPolicyId = agentPolicyResponse.data.item.id; + + const packagePolicyResponse = await kbnClient.request<{ item: { id: string } }>({ + method: 'POST', + path: '/api/fleet/package_policies', + query: { + format: 'simplified', + }, + body: { + policy_ids: [agentPolicyId], + package: { + name: 'nginx', + version: '2.3.2', + }, + name: packagePolicyName, + description: '', + namespace: 'default', + inputs: { + 'nginx-logfile': { + enabled: true, + streams: { + 'nginx.access': { + enabled: true, + vars: { + paths: ['/var/log/nginx/test123-access.log*'], + tags: ['nginx-access'], + preserve_original_event: false, + ignore_older: '72h', + }, + }, + 'nginx.error': { + enabled: false, + vars: { + paths: ['/var/log/nginx/error.log*'], + tags: ['nginx-error'], + preserve_original_event: false, + ignore_older: '72h', + }, + }, + }, + }, + 'nginx-nginx/metrics': { + enabled: false, + vars: { + hosts: ['http://127.0.0.1:80'], + }, + streams: { + 'nginx.stubstatus': { + enabled: false, + vars: { + period: '10s', + server_status_path: '/nginx_status', + tags: ['nginx-stubstatus'], + }, + }, + }, + }, + }, + }, + }); + packagePolicyId = packagePolicyResponse.data.item.id; + }); + + test.afterAll(async ({ kbnClient }) => { + if (packagePolicyId) { + await kbnClient.request({ + method: 'POST', + path: '/api/fleet/package_policies/delete', + body: { + packagePolicyIds: [packagePolicyId], + }, + }); + } + + const packagePoliciesResponse = await kbnClient.request<{ + items: Array<{ id: string; name: string }>; + }>({ + method: 'GET', + path: '/api/fleet/package_policies', + query: { + kuery: `ingest-package-policies.name:${packagePolicyName}*`, + }, + }); + for (const policy of packagePoliciesResponse.data.items) { + if (policy.name.startsWith(packagePolicyName)) { + await kbnClient.request({ + method: 'POST', + path: '/api/fleet/package_policies/delete', + body: { + packagePolicyIds: [policy.id], + }, + }); + } + } + + if (agentPolicyId) { + await kbnClient.request({ + method: 'POST', + path: '/api/fleet/agent_policies/delete', + body: { + agentPolicyId, + }, + }); + } + }); + + test('can copy nginx package policy', async ({ browserAuth, pageObjects }) => { + await browserAuth.loginAsPrivilegedUser(); + const { copyIntegration } = pageObjects; + + await copyIntegration.navigateTo(packagePolicyId); + await copyIntegration.waitForPageToLoad(); + + const nameInput = copyIntegration.getPackagePolicyNameInput(); + await expect(nameInput).toBeVisible(); + + await expect(nameInput).toHaveValue(`copy-${packagePolicyName}`); + + const pathsInput = copyIntegration.getMultiTextInputRow('nginx.access', 'paths'); + await expect(pathsInput).toHaveValue('/var/log/nginx/test123-access.log*'); + + await expect(copyIntegration.getSaveButton()).toBeVisible(); + await copyIntegration.clickSaveButton(); + + await expect(copyIntegration.getSuccessPostInstallAddAgentModal()).toBeVisible(); + }); +}); From f47d63fd55bb35cf19ba634ab0c6cb1ccbad6c81 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 19 Jan 2026 15:27:43 -0500 Subject: [PATCH 5/9] fix breadcrumb --- .../fleet/hooks/use_breadcrumbs.tsx | 8 +++++ .../copy_package_policy_page/index.tsx | 35 +++++++++++++++++++ .../single_page_layout/index.tsx | 3 +- .../create_package_policy_page/types.ts | 1 + 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx index 49af462dc96f5..ad1996555d3ab 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx @@ -79,6 +79,14 @@ const breadcrumbGetters: { }), }, ], + copy_integration: ({ policyName, policyId }) => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.fleet.breadcrumbs.copyPackagePolicyPageTitle', { + defaultMessage: 'Copy integration', + }), + }, + ], edit_integration: ({ policyName, policyId }) => [ BASE_BREADCRUMB, { diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx index 0c56b067b080b..f254165aec37f 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx @@ -8,11 +8,23 @@ import React, { useMemo, memo } from 'react'; import { useRouteMatch, useLocation } from 'react-router-dom'; +import { EuiEmptyPrompt, EuiFlexGroup } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import styled from '@emotion/styled'; + +import { EXCLUDED_FROM_PACKAGE_POLICY_COPY_PACKAGES } from '../../../../../../common/constants'; + import { useGetOnePackagePolicy } from '../../../../integrations/hooks'; import { Loading } from '../../../components'; import type { EditPackagePolicyFrom } from '../create_package_policy_page/types'; import { CreatePackagePolicySinglePage } from '../create_package_policy_page/single_page_layout'; +import { useBreadcrumbs } from '../../../hooks'; + +const ContentWrapper = styled(EuiFlexGroup)` + height: 100%; + margin: 0 auto; +`; export const CopyPackagePolicyPage = memo(() => { const { @@ -21,6 +33,10 @@ export const CopyPackagePolicyPage = memo(() => { const packagePolicy = useGetOnePackagePolicy(packagePolicyId); + useBreadcrumbs('copy_integration', { + policyId: packagePolicyId, + }); + const packagePolicyData = useMemo(() => { if (packagePolicy.data?.item) { return { @@ -38,6 +54,24 @@ export const CopyPackagePolicyPage = memo(() => { if (packagePolicy.isLoading) { return ; } + const pkgName = packagePolicy.data?.item?.package?.name; + + if (pkgName && EXCLUDED_FROM_PACKAGE_POLICY_COPY_PACKAGES.includes(pkgName)) { + return ( + + + } + color="danger" + iconType="error" + /> + + ); + } return ( { pkgName={packagePolicy.data!.item!.package!.name} pkgVersion={packagePolicy.data!.item!.package!.version} defaultPolicyData={packagePolicyData} + noBreadcrumbs={true} prerelease={true} /> ); diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx index ad6b4a31757ac..8bbddc9e20b4a 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx @@ -116,6 +116,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ pkgLabel, addIntegrationFlyoutProps, defaultPolicyData, + noBreadcrumbs, }) => { const { agents: { enabled: isFleetEnabled }, @@ -745,7 +746,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} /> )} - {packageInfo && !addIntegrationFlyoutProps && ( + {packageInfo && !addIntegrationFlyoutProps && !noBreadcrumbs && ( ; + noBreadcrumbs?: boolean; addIntegrationFlyoutProps?: { selectIntegrationStep: EuiStepProps; onSubmitCompleted: () => void; From 3c0f6d2d4f49ca17f5b1df6601350e8d46cd5954 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 19 Jan 2026 15:28:40 -0500 Subject: [PATCH 6/9] fix breadcrumb --- .../sections/agent_policy/copy_package_policy_page/index.tsx | 2 +- .../create_package_policy_page/single_page_layout/index.tsx | 4 ++-- .../sections/agent_policy/create_package_policy_page/types.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx index f254165aec37f..58045492b4acf 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx @@ -79,7 +79,7 @@ export const CopyPackagePolicyPage = memo(() => { pkgName={packagePolicy.data!.item!.package!.name} pkgVersion={packagePolicy.data!.item!.package!.version} defaultPolicyData={packagePolicyData} - noBreadcrumbs={true} + noBreadcrumb={true} prerelease={true} /> ); diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx index 8bbddc9e20b4a..95a63df298465 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx @@ -116,7 +116,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ pkgLabel, addIntegrationFlyoutProps, defaultPolicyData, - noBreadcrumbs, + noBreadcrumb, }) => { const { agents: { enabled: isFleetEnabled }, @@ -746,7 +746,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} /> )} - {packageInfo && !addIntegrationFlyoutProps && !noBreadcrumbs && ( + {packageInfo && !addIntegrationFlyoutProps && !noBreadcrumb && ( ; - noBreadcrumbs?: boolean; + noBreadcrumb?: boolean; addIntegrationFlyoutProps?: { selectIntegrationStep: EuiStepProps; onSubmitCompleted: () => void; From 2823842a6c2bf7d295c4583b3cfc1978dc03e9e2 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 20 Jan 2026 09:45:24 -0500 Subject: [PATCH 7/9] fix breadcrumbs --- .../fleet/hooks/use_breadcrumbs.tsx | 22 +++-- .../copy_package_policy_page/index.tsx | 80 ++++++++++++++----- .../create_package_policy_page/types.ts | 3 +- .../integrations/hooks/use_breadcrumbs.tsx | 28 +++++++ .../integrations/sections/epm/index.tsx | 5 ++ .../package_policy_actions_menu.tsx | 14 +++- .../fleet/public/constants/page_paths.ts | 18 ++++- 7 files changed, 138 insertions(+), 32 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx index ad1996555d3ab..51847fad49740 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx @@ -79,15 +79,25 @@ const breadcrumbGetters: { }), }, ], - copy_integration: ({ policyName, policyId }) => [ + edit_integration: ({ policyName, policyId }) => [ BASE_BREADCRUMB, { - text: i18n.translate('xpack.fleet.breadcrumbs.copyPackagePolicyPageTitle', { - defaultMessage: 'Copy integration', + href: pagePathGetters.policies()[1], + text: i18n.translate('xpack.fleet.breadcrumbs.policiesPageTitle', { + defaultMessage: 'Agent policies', + }), + }, + { + href: pagePathGetters.policy_details({ policyId })[1], + text: policyName, + }, + { + text: i18n.translate('xpack.fleet.breadcrumbs.editPackagePolicyPageTitle', { + defaultMessage: 'Edit integration', }), }, ], - edit_integration: ({ policyName, policyId }) => [ + copy_integration: ({ policyName, policyId }) => [ BASE_BREADCRUMB, { href: pagePathGetters.policies()[1], @@ -100,8 +110,8 @@ const breadcrumbGetters: { text: policyName, }, { - text: i18n.translate('xpack.fleet.breadcrumbs.editPackagePolicyPageTitle', { - defaultMessage: 'Edit integration', + text: i18n.translate('xpack.fleet.breadcrumbs.copyPackagePolicyPageTitle', { + defaultMessage: 'Copy integration', }), }, ], diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx index 58045492b4acf..af19930536ee1 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx @@ -19,23 +19,45 @@ import { Loading } from '../../../components'; import type { EditPackagePolicyFrom } from '../create_package_policy_page/types'; import { CreatePackagePolicySinglePage } from '../create_package_policy_page/single_page_layout'; -import { useBreadcrumbs } from '../../../hooks'; +import { useBreadcrumbs, useGetOneAgentPolicy } from '../../../hooks'; +import { useBreadcrumbs as useIntegrationsBreadcrumbs } from '../../../../integrations/hooks'; const ContentWrapper = styled(EuiFlexGroup)` height: 100%; margin: 0 auto; `; +const IntegrationsBreadcrumb = memo<{ + pkgTitle: string; + policyName: string; + pkgkey: string; +}>(({ pkgTitle, policyName, pkgkey }) => { + useIntegrationsBreadcrumbs('integration_policy_copy', { policyName, pkgTitle, pkgkey }); + return null; +}); + +const PoliciesBreadcrumb: React.FunctionComponent<{ + policyName: string; + policyId: string; +}> = ({ policyName, policyId }) => { + useBreadcrumbs('copy_integration', { policyName, policyId }); + return null; +}; + +const InstalledIntegrationsBreadcrumb = memo<{ + policyName: string; +}>(({ policyName }) => { + useIntegrationsBreadcrumbs('integration_policy_edit_from_installed', { policyName }); + return null; +}); + export const CopyPackagePolicyPage = memo(() => { const { - params: { packagePolicyId }, - } = useRouteMatch<{ packagePolicyId: string }>(); + params: { packagePolicyId, policyId }, + } = useRouteMatch<{ packagePolicyId: string; policyId?: string }>(); const packagePolicy = useGetOnePackagePolicy(packagePolicyId); - - useBreadcrumbs('copy_integration', { - policyId: packagePolicyId, - }); + const agentPolicy = useGetOneAgentPolicy(policyId); const packagePolicyData = useMemo(() => { if (packagePolicy.data?.item) { @@ -49,16 +71,35 @@ export const CopyPackagePolicyPage = memo(() => { // Parse the 'from' query parameter to determine navigation after save const { search } = useLocation(); const qs = new URLSearchParams(search); - const fromQs = qs.get('from') as EditPackagePolicyFrom | null; + const fromQs = (qs.get('from') as EditPackagePolicyFrom | null) ?? 'fleet-policy-list'; - if (packagePolicy.isLoading) { - return ; + if (packagePolicy.isLoading || !packagePolicy.data) { + return ( + <> + + + ); } + + const breadcrumb = + fromQs === 'fleet-policy-list' && policyId ? ( + + ) : fromQs === 'installed-integrations' ? ( + + ) : ( + + ); + const pkgName = packagePolicy.data?.item?.package?.name; if (pkgName && EXCLUDED_FROM_PACKAGE_POLICY_COPY_PACKAGES.includes(pkgName)) { return ( + {breadcrumb} { } return ( - + <> + {breadcrumb} + + ); }); diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts index f6bf6c469d974..d0f660392a3c5 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts @@ -18,8 +18,7 @@ export type EditPackagePolicyFrom = | 'upgrade-from-fleet-policy-list' | 'upgrade-from-integrations-policy-list' | 'upgrade-from-extension' - | 'copy-from-fleet-policy-list' - | 'copy-from-integrations-policy-list' + | 'fleet-policy-list' | 'installed-integrations'; export type PackagePolicyFormState = diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/hooks/use_breadcrumbs.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/hooks/use_breadcrumbs.tsx index f730cd7ece2e4..cb8f76f9c8541 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/hooks/use_breadcrumbs.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/hooks/use_breadcrumbs.tsx @@ -59,6 +59,19 @@ const breadcrumbGetters: { }, { text: policyName }, ], + integration_policy_copy: ({ pkgTitle, pkgkey, policyName }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.integration_details_policies({ pkgkey })[1], + text: pkgTitle, + }, + { text: policyName }, + { + text: i18n.translate('xpack.fleet.breadcrumbs.copyPackagePolicyPageTitle', { + defaultMessage: 'Copy integration', + }), + }, + ], integration_policy_upgrade: ({ pkgTitle, pkgkey, policyName }) => [ BASE_BREADCRUMB, { @@ -82,6 +95,21 @@ const breadcrumbGetters: { }, { text: policyName }, ], + integration_policy_copy_from_installed: ({ policyName }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.integrations_installed({})[1], + text: i18n.translate('xpack.fleet.breadcrumbs.installedIntegrationsPageTitle', { + defaultMessage: 'Installed integrations', + }), + }, + { text: policyName }, + { + text: i18n.translate('xpack.fleet.breadcrumbs.copyPackagePolicyPageTitle', { + defaultMessage: 'Copy integration', + }), + }, + ], }; export function useBreadcrumbs(page: Page, values: DynamicPagePathValues = {}) { diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/index.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/index.tsx index 303951eaf9461..bb0fba95bb639 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/index.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/index.tsx @@ -13,6 +13,8 @@ import { EuiSkeletonText } from '@elastic/eui'; import { INTEGRATIONS_ROUTING_PATHS } from '../../constants'; import { IntegrationsStateContextProvider, useBreadcrumbs, useStartServices } from '../../hooks'; +import { CopyPackagePolicyPage } from '../../../fleet/sections/agent_policy/copy_package_policy_page'; + import { EPMHomePage } from './screens/home'; import { Detail } from './screens/detail'; import { Policy } from './screens/policy'; @@ -28,6 +30,9 @@ export const EPMApp: React.FunctionComponent = () => { + + + diff --git a/x-pack/platform/plugins/shared/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/platform/plugins/shared/fleet/public/components/package_policy_actions_menu.tsx index f2721b2838e2b..cbefaf98e6e7d 100644 --- a/x-pack/platform/plugins/shared/fleet/public/components/package_policy_actions_menu.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/components/package_policy_actions_menu.tsx @@ -132,9 +132,17 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ !canWriteIntegrationPolicies || EXCLUDED_FROM_PACKAGE_POLICY_COPY_PACKAGES.includes(packagePolicy.package?.name || '') } - href={getHref('copy_integration', { - packagePolicyId: packagePolicy.id, - })} + href={ + isOrphanedPolicy || isAgentlessPolicy + ? getHref('integration_policy_copy', { + policyId: agentPolicy?.id || '', + packagePolicyId: packagePolicy.id, + }) + (from ? `?from=${from}` : '') + : getHref('copy_integration', { + policyId: agentPolicy?.id || '', + packagePolicyId: packagePolicy.id, + }) + (from ? `?from=${from}` : '') + } data-test-subj="PackagePolicyActionsCopyItem" icon="copy" key="packagePolicyCopy" diff --git a/x-pack/platform/plugins/shared/fleet/public/constants/page_paths.ts b/x-pack/platform/plugins/shared/fleet/public/constants/page_paths.ts index 99071fb1b442f..9ae272bb056d7 100644 --- a/x-pack/platform/plugins/shared/fleet/public/constants/page_paths.ts +++ b/x-pack/platform/plugins/shared/fleet/public/constants/page_paths.ts @@ -37,8 +37,10 @@ export type DynamicPage = | 'integration_details_api_reference' | 'integration_details_configs' | 'integration_policy_edit' + | 'integration_policy_copy' | 'integration_policy_upgrade' | 'integration_policy_edit_from_installed' + | 'integration_policy_copy_from_installed' | 'policy_details' | 'add_integration_to_policy' | 'edit_integration' @@ -77,7 +79,7 @@ export const FLEET_ROUTING_PATHS = { policy_details: '/policies/:policyId/:tabId?', policy_details_settings: '/policies/:policyId/settings', edit_integration: '/policies/:policyId/edit-integration/:packagePolicyId', - copy_integration: '/policies/copy-integration/:packagePolicyId', + copy_integration: '/policies/:policyId/copy-integration/:packagePolicyId', upgrade_package_policy: '/policies/:policyId/upgrade-package-policy/:packagePolicyId', enrollment_tokens: '/enrollment-tokens', uninstall_tokens: '/uninstall-tokens', @@ -115,6 +117,7 @@ export const INTEGRATIONS_ROUTING_PATHS = { integration_details_api_reference: '/detail/:pkgkey/api-reference', integration_details_language_clients: '/language_clients/:pkgkey/overview', integration_policy_edit: '/edit-integration/:packagePolicyId', + integration_policy_copy: '/copy-integration/:packagePolicyId', integration_policy_upgrade: '/edit-integration/:packagePolicyId', }; @@ -211,6 +214,10 @@ export const pagePathGetters: { INTEGRATIONS_BASE_PATH, `/edit-integration/${packagePolicyId}`, ], + integration_policy_copy: ({ packagePolicyId }) => [ + INTEGRATIONS_BASE_PATH, + `/copy-integration/${packagePolicyId}`, + ], // Upgrades happen on the same edit form, just with a flag set. Separate page record here // allows us to set different breadcrumbs for upgrades when needed. integration_policy_upgrade: ({ packagePolicyId }) => [ @@ -222,6 +229,11 @@ export const pagePathGetters: { INTEGRATIONS_BASE_PATH, `/edit-integration/${packagePolicyId}`, ], + // Used for breadcrumbs when copying a policy from the installed integrations tab + integration_policy_copy_from_installed: ({ packagePolicyId }) => [ + INTEGRATIONS_BASE_PATH, + `/edit-integration/${packagePolicyId}`, + ], // This route allows rendering custom language integration pages registered in the language_client plugin integration_details_language_clients: ({ pkgkey }) => [ INTEGRATIONS_BASE_PATH, @@ -255,9 +267,9 @@ export const pagePathGetters: { FLEET_BASE_PATH, `/policies/${policyId}/edit-integration/${packagePolicyId}`, ], - copy_integration: ({ packagePolicyId }) => [ + copy_integration: ({ policyId, packagePolicyId }) => [ FLEET_BASE_PATH, - `/policies/copy-integration/${packagePolicyId}`, + `/policies/${policyId}/copy-integration/${packagePolicyId}`, ], upgrade_package_policy: ({ policyId, packagePolicyId }) => [ FLEET_BASE_PATH, From c1e604fb734213b22817991db98e7241a7af9782 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 20 Jan 2026 09:54:07 -0500 Subject: [PATCH 8/9] fix tooltip --- .../public/components/package_policy_actions_menu.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/x-pack/platform/plugins/shared/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/platform/plugins/shared/fleet/public/components/package_policy_actions_menu.tsx index cbefaf98e6e7d..72297331aae5d 100644 --- a/x-pack/platform/plugins/shared/fleet/public/components/package_policy_actions_menu.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/components/package_policy_actions_menu.tsx @@ -132,6 +132,15 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ !canWriteIntegrationPolicies || EXCLUDED_FROM_PACKAGE_POLICY_COPY_PACKAGES.includes(packagePolicy.package?.name || '') } + toolTipContent={ + EXCLUDED_FROM_PACKAGE_POLICY_COPY_PACKAGES.includes(packagePolicy.package?.name || '') ? ( + + ) : undefined + } href={ isOrphanedPolicy || isAgentlessPolicy ? getHref('integration_policy_copy', { From 0641dcb4ca8168283233832a3341925b3f6001ca Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 20 Jan 2026 13:00:43 -0500 Subject: [PATCH 9/9] fix breadcrumbs --- .../copy_package_policy_page/index.tsx | 23 ++++++++++++++----- .../hooks/navigation.tsx | 7 +++++- .../single_page_layout/components/layout.tsx | 2 +- .../create_package_policy_page/types.ts | 3 +++ .../page_objects/copy_integration_page.ts | 4 ++-- .../scout/ui/tests/copy_integration.spec.ts | 2 +- 6 files changed, 30 insertions(+), 11 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx index af19930536ee1..b55dfa9d4f301 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx @@ -47,7 +47,7 @@ const PoliciesBreadcrumb: React.FunctionComponent<{ const InstalledIntegrationsBreadcrumb = memo<{ policyName: string; }>(({ policyName }) => { - useIntegrationsBreadcrumbs('integration_policy_edit_from_installed', { policyName }); + useIntegrationsBreadcrumbs('integration_policy_copy_from_installed', { policyName }); return null; }); @@ -70,8 +70,19 @@ export const CopyPackagePolicyPage = memo(() => { // Parse the 'from' query parameter to determine navigation after save const { search } = useLocation(); - const qs = new URLSearchParams(search); - const fromQs = (qs.get('from') as EditPackagePolicyFrom | null) ?? 'fleet-policy-list'; + + const from = useMemo(() => { + const qs = new URLSearchParams(search); + const qsFrom = (qs.get('from') as EditPackagePolicyFrom | null) ?? 'fleet-policy-list'; + + if (qsFrom === 'fleet-policy-list') { + return 'copy-from-fleet-policy-list'; + } else if (qsFrom === 'installed-integrations') { + return 'copy-from-installed-integrations'; + } else { + return 'copy-from-integrations-policy-list'; + } + }, [search]); if (packagePolicy.isLoading || !packagePolicy.data) { return ( @@ -82,9 +93,9 @@ export const CopyPackagePolicyPage = memo(() => { } const breadcrumb = - fromQs === 'fleet-policy-list' && policyId ? ( + from === 'copy-from-fleet-policy-list' && policyId ? ( - ) : fromQs === 'installed-integrations' ? ( + ) : from === 'copy-from-installed-integrations' ? ( ) : ( { <> {breadcrumb} { if (routeState && routeState.onCancelUrl) { return routeState.onCancelUrl; } - return from === 'policy' && agentPolicyId + if (from === 'installed-integrations' || from === 'copy-from-installed-integrations') { + return `${getHref('integrations_installed', {})}?viewPolicies=${splitPkgKey(pkgkey).pkgName}`; + } + + return (from === 'policy' || from === 'copy-from-fleet-policy-list') && agentPolicyId ? getHref('policy_details', { policyId: agentPolicyId, }) diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/layout.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/layout.tsx index 2c92829dfc685..1fc5bc6bf2076 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/layout.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/layout.tsx @@ -84,7 +84,7 @@ export const CreatePackagePolicySinglePageLayout: React.FunctionComponent<{ [ 'copy-from-fleet-policy-list', 'copy-from-integrations-policy-list', - 'copy-from-extension', + 'copy-from-installed-integrations', ].includes(from), [from] ); diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts index d0f660392a3c5..98bc2991ca7c1 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts @@ -18,6 +18,9 @@ export type EditPackagePolicyFrom = | 'upgrade-from-fleet-policy-list' | 'upgrade-from-integrations-policy-list' | 'upgrade-from-extension' + | 'copy-from-fleet-policy-list' + | 'copy-from-integrations-policy-list' + | 'copy-from-installed-integrations' | 'fleet-policy-list' | 'installed-integrations'; diff --git a/x-pack/platform/plugins/shared/fleet/test/scout/ui/fixtures/page_objects/copy_integration_page.ts b/x-pack/platform/plugins/shared/fleet/test/scout/ui/fixtures/page_objects/copy_integration_page.ts index 4381bd8c1ffc7..b35941a494cd3 100644 --- a/x-pack/platform/plugins/shared/fleet/test/scout/ui/fixtures/page_objects/copy_integration_page.ts +++ b/x-pack/platform/plugins/shared/fleet/test/scout/ui/fixtures/page_objects/copy_integration_page.ts @@ -10,8 +10,8 @@ import { type ScoutPage } from '@kbn/scout'; export class CopyIntegrationPage { constructor(private readonly page: ScoutPage) {} - async navigateTo(packagePolicyId: string) { - await this.page.gotoApp(`fleet/policies/copy-integration/${packagePolicyId}`); + async navigateTo(agentPolicyId: string, packagePolicyId: string) { + await this.page.gotoApp(`fleet/policies/${agentPolicyId}/copy-integration/${packagePolicyId}`); } async waitForPageToLoad() { diff --git a/x-pack/platform/plugins/shared/fleet/test/scout/ui/tests/copy_integration.spec.ts b/x-pack/platform/plugins/shared/fleet/test/scout/ui/tests/copy_integration.spec.ts index e006a7f586137..63fca8a184497 100644 --- a/x-pack/platform/plugins/shared/fleet/test/scout/ui/tests/copy_integration.spec.ts +++ b/x-pack/platform/plugins/shared/fleet/test/scout/ui/tests/copy_integration.spec.ts @@ -135,7 +135,7 @@ test.describe('Copy integration', { tag: ['@ess'] }, () => { await browserAuth.loginAsPrivilegedUser(); const { copyIntegration } = pageObjects; - await copyIntegration.navigateTo(packagePolicyId); + await copyIntegration.navigateTo(agentPolicyId, packagePolicyId); await copyIntegration.waitForPageToLoad(); const nameInput = copyIntegration.getPackagePolicyNameInput();