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/hooks/use_breadcrumbs.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx
index 49af462dc96f5..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
@@ -97,6 +97,24 @@ const breadcrumbGetters: {
}),
},
],
+ copy_integration: ({ policyName, policyId }) => [
+ BASE_BREADCRUMB,
+ {
+ 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.copyPackagePolicyPageTitle', {
+ defaultMessage: 'Copy integration',
+ }),
+ },
+ ],
upgrade_package_policy: ({ 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
new file mode 100644
index 0000000000000..b55dfa9d4f301
--- /dev/null
+++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/copy_package_policy_page/index.tsx
@@ -0,0 +1,141 @@
+/*
+ * 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 { 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, 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_copy_from_installed', { policyName });
+ return null;
+});
+
+export const CopyPackagePolicyPage = memo(() => {
+ const {
+ params: { packagePolicyId, policyId },
+ } = useRouteMatch<{ packagePolicyId: string; policyId?: string }>();
+
+ const packagePolicy = useGetOnePackagePolicy(packagePolicyId);
+ const agentPolicy = useGetOneAgentPolicy(policyId);
+
+ const packagePolicyData = useMemo(() => {
+ if (packagePolicy.data?.item) {
+ return {
+ ...packagePolicy.data.item,
+ name: 'copy-' + packagePolicy.data.item.name,
+ };
+ }
+ }, [packagePolicy.data?.item]);
+
+ // Parse the 'from' query parameter to determine navigation after save
+ const { search } = useLocation();
+
+ 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 (
+ <>
+
+ >
+ );
+ }
+
+ const breadcrumb =
+ from === 'copy-from-fleet-policy-list' && policyId ? (
+
+ ) : from === 'copy-from-installed-integrations' ? (
+
+ ) : (
+
+ );
+
+ const pkgName = packagePolicy.data?.item?.package?.name;
+
+ if (pkgName && EXCLUDED_FROM_PACKAGE_POLICY_COPY_PACKAGES.includes(pkgName)) {
+ return (
+
+ {breadcrumb}
+
+ }
+ color="danger"
+ iconType="error"
+ />
+
+ );
+ }
+
+ return (
+ <>
+ {breadcrumb}
+
+ >
+ );
+});
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/public/applications/fleet/sections/agent_policy/create_package_policy_page/hooks/navigation.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/hooks/navigation.tsx
index 6cd5d2bb5087a..1c8b487c0bb50 100644
--- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/hooks/navigation.tsx
+++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/hooks/navigation.tsx
@@ -8,6 +8,7 @@
import { useCallback, useMemo, useEffect, useRef } from 'react';
import type { ApplicationStart } from '@kbn/core-application-browser';
+import { splitPkgKey } from '../../../../../../../common/services';
import { PLUGIN_ID, INTEGRATIONS_PLUGIN_ID } from '../../../../constants';
import { pkgKeyFromPackageInfo } from '../../../../services';
import { useStartServices, useLink, useIntraAppState } from '../../../../hooks';
@@ -48,7 +49,11 @@ export const useCancelAddPackagePolicy = (params: UseCancelParams) => {
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 94d38ac74a569..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
@@ -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?: Partial;
'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-installed-integrations',
+ ].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 (
@@ -168,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/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..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';
@@ -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,29 @@ export function useOnSubmit({
integrationToEnable
);
+ if (defaultPolicyData) {
+ 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
updatePackagePolicy(basePackagePolicy);
setIsInitialized(true);
@@ -440,6 +465,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..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
@@ -115,6 +115,8 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
integration,
pkgLabel,
addIntegrationFlyoutProps,
+ defaultPolicyData,
+ noBreadcrumb,
}) => {
const {
agents: { enabled: isFleetEnabled },
@@ -144,7 +146,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 +221,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
setNewAgentPolicy,
setSelectedPolicyTab,
isAddIntegrationFlyout,
+ defaultPolicyData,
});
if (addIntegrationFlyoutProps?.agentPolicy) {
@@ -361,8 +366,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 +392,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 +412,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
updateSelectedPolicyTab,
queryParamsPolicyId,
setHasAgentPolicyError,
+ defaultPolicyData,
]
);
@@ -725,7 +746,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
onCancel={() => navigateAddAgentHelp(savedPackagePolicy)}
/>
)}
- {packageInfo && !addIntegrationFlyoutProps && (
+ {packageInfo && !addIntegrationFlyoutProps && !noBreadcrumb && (
;
+ noBreadcrumb?: boolean;
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/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.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..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
@@ -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,40 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{
,
]
: []),
- // FIXME: implement Copy package policy action
- // {}} key="packagePolicyCopy">
- //
- // ,
+
+ ) : undefined
+ }
+ 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"
+ >
+
+ ,
];
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..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,11 +37,14 @@ 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'
+ | 'copy_integration'
| 'upgrade_package_policy'
| 'agent_list'
| 'agent_details'
@@ -76,6 +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/:policyId/copy-integration/:packagePolicyId',
upgrade_package_policy: '/policies/:policyId/upgrade-package-policy/:packagePolicyId',
enrollment_tokens: '/enrollment-tokens',
uninstall_tokens: '/uninstall-tokens',
@@ -113,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',
};
@@ -209,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 }) => [
@@ -220,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,
@@ -253,6 +267,10 @@ export const pagePathGetters: {
FLEET_BASE_PATH,
`/policies/${policyId}/edit-integration/${packagePolicyId}`,
],
+ copy_integration: ({ policyId, packagePolicyId }) => [
+ FLEET_BASE_PATH,
+ `/policies/${policyId}/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,
+ }
);
};
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..b35941a494cd3
--- /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(agentPolicyId: string, packagePolicyId: string) {
+ await this.page.gotoApp(`fleet/policies/${agentPolicyId}/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 6ee2b2c50f7a8..5c3d69e218254 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,6 +9,7 @@ import type { PageObjects, ScoutPage } from '@kbn/scout';
import { createLazyPageObject } from '@kbn/scout';
import { BrowseIntegrationPage } from './browse_integrations_page';
+import { CopyIntegrationPage } from './copy_integration_page';
import { CreateIntegrationLandingPage } from './create_integration_landing_page';
import { FleetHomePage } from './fleet_home';
import { IntegrationHomePage } from './integration_home';
@@ -18,12 +19,14 @@ export interface StreamsPageObjects extends PageObjects {
createIntegrationLanding: CreateIntegrationLandingPage;
fleetHome: FleetHomePage;
integrationHome: IntegrationHomePage;
+ copyIntegration: CopyIntegrationPage;
}
export function extendPageObjects(pageObjects: PageObjects, page: ScoutPage): StreamsPageObjects {
return {
...pageObjects,
browseIntegrations: createLazyPageObject(BrowseIntegrationPage, page),
+ copyIntegration: createLazyPageObject(CopyIntegrationPage, page),
createIntegrationLanding: createLazyPageObject(CreateIntegrationLandingPage, page),
fleetHome: createLazyPageObject(FleetHomePage, page),
integrationHome: createLazyPageObject(IntegrationHomePage, 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..63fca8a184497
--- /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(agentPolicyId, 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();
+ });
+});