diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 9adc075a7983f8..f9127e4629f43e 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -34,7 +34,7 @@ pageLoadAssetSize: indexManagement: 140608 indexPatternManagement: 28222 infra: 184320 - fleet: 450005 + fleet: 465774 ingestPipelines: 58003 inputControlVis: 172675 inspector: 148711 diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 83875801300d32..aece6580831960 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -43,7 +43,7 @@ export type InstallSource = 'registry' | 'upload'; export type EpmPackageInstallStatus = 'installed' | 'installing'; -export type DetailViewPanelName = 'overview' | 'policies' | 'settings' | 'custom'; +export type DetailViewPanelName = 'overview' | 'policies' | 'assets' | 'settings' | 'custom'; export type ServiceName = 'kibana' | 'elasticsearch'; export type AgentAssetType = typeof agentAssetTypes; export type DocAssetType = 'doc' | 'notice'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index 75fc06c1a44945..b3b0d6ed51cb4d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -19,10 +19,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, + EuiLink, } from '@elastic/eui'; import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; import type { ApplicationStart } from 'kibana/public'; +import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; import type { AgentPolicy, PackageInfo, @@ -60,7 +62,7 @@ const StepsWithLessPadding = styled(EuiSteps)` `; const CustomEuiBottomBar = styled(EuiBottomBar)` - // Set a relatively _low_ z-index value here to account for EuiComboBox popover that might appear under the bottom bar + /* A relatively _low_ z-index value here to account for EuiComboBox popover that might appear under the bottom bar */ z-index: 50; `; @@ -84,11 +86,26 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { const history = useHistory(); const handleNavigateTo = useNavigateToCallback(); const routeState = useIntraAppState(); - const from: CreatePackagePolicyFrom = 'policyId' in params ? 'policy' : 'package'; const { search } = useLocation(); const queryParams = useMemo(() => new URLSearchParams(search), [search]); - const policyId = useMemo(() => queryParams.get('policyId') ?? undefined, [queryParams]); + const queryParamsPolicyId = useMemo(() => queryParams.get('policyId') ?? undefined, [ + queryParams, + ]); + + /** + * Please note: policyId can come from one of two sources. The URL param (in the URL path) or + * in the query params (?policyId=foo). + * + * Either way, we take this as an indication that a user is "coming from" the fleet policy UI + * since we link them out to packages (a.k.a. integrations) UI when choosing a new package. It is + * no longer possible to choose a package directly in the create package form. + * + * We may want to deprecate the ability to pass in policyId from URL params since there is no package + * creation possible if a user has not chosen one from the packages UI. + */ + const from: CreatePackagePolicyFrom = + 'policyId' in params || queryParamsPolicyId ? 'policy' : 'package'; // Agent policy and package info states const [agentPolicy, setAgentPolicy] = useState(); @@ -280,6 +297,13 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { ); } + const fromPolicyWithoutAgentsAssigned = from === 'policy' && agentPolicy && agentCount === 0; + + const fromPackageWithoutAgentsAssigned = + from === 'package' && packageInfo && agentPolicy && agentCount === 0; + + const hasAgentsAssigned = agentCount && agentPolicy; + notifications.toasts.addSuccess({ title: i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationTitle', { defaultMessage: `'{packagePolicyName}' integration added.`, @@ -287,22 +311,47 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { packagePolicyName: packagePolicy.name, }, }), - text: - agentCount && agentPolicy - ? i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationMessage', { - defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy.`, - values: { - agentPolicyName: agentPolicy.name, - }, - }) - : (params as AddToPolicyParams)?.policyId && agentPolicy && agentCount === 0 - ? i18n.translate('xpack.fleet.createPackagePolicy.addAgentNextNotification', { + text: fromPolicyWithoutAgentsAssigned + ? i18n.translate( + 'xpack.fleet.createPackagePolicy.policyContextAddAgentNextNotificationMessage', + { defaultMessage: `The policy has been updated. Add an agent to the '{agentPolicyName}' policy to deploy this policy.`, values: { - agentPolicyName: agentPolicy.name, + agentPolicyName: agentPolicy!.name, }, - }) - : undefined, + } + ) + : fromPackageWithoutAgentsAssigned + ? toMountPoint( + // To render the link below we need to mount this JSX in the success toast + + {i18n.translate( + 'xpack.fleet.createPackagePolicy.integrationsContextAddAgentLinkMessage', + { defaultMessage: 'add an agent' } + )} + + ), + }} + /> + ) + : hasAgentsAssigned + ? i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationMessage', { + defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy.`, + values: { + agentPolicyName: agentPolicy!.name, + }, + }) + : undefined, 'data-test-subj': 'packagePolicyCreateSuccessToast', }); } else { @@ -312,6 +361,9 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { setFormState('VALID'); } }, [ + getHref, + from, + packageInfo, agentCount, agentPolicy, formState, @@ -353,13 +405,13 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { ), - [params, updatePackageInfo, agentPolicy, updateAgentPolicy, policyId] + [params, updatePackageInfo, agentPolicy, updateAgentPolicy, queryParamsPolicyId] ); const ExtensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-create'); @@ -455,7 +507,8 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { )} - + + diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx index 54adbd78ab75aa..39340a21d349bd 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx @@ -9,10 +9,11 @@ import React, { memo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; -import { useCapabilities, useLink } from '../../../../../hooks'; +import { useCapabilities, useStartServices } from '../../../../../hooks'; +import { pagePathGetters, INTEGRATIONS_PLUGIN_ID } from '../../../../../constants'; export const NoPackagePolicies = memo<{ policyId: string }>(({ policyId }) => { - const { getHref } = useLink(); + const { application } = useStartServices(); const hasWriteCapabilities = useCapabilities().write; return ( @@ -36,7 +37,12 @@ export const NoPackagePolicies = memo<{ policyId: string }>(({ policyId }) => { + application.navigateToApp(INTEGRATIONS_PLUGIN_ID, { + path: `#${pagePathGetters.integrations_all()[1]}`, + state: { forAgentPolicyId: policyId }, + }) + } > { + const { name, version } = packageInfo; + const { + savedObjects: { client: savedObjectsClient }, + } = useStartServices(); + + const { getPath } = useLink(); + const getPackageInstallStatus = useGetPackageInstallStatus(); + const packageInstallStatus = getPackageInstallStatus(packageInfo.name); + + const [assetSavedObjects, setAssetsSavedObjects] = useState(); + const [fetchError, setFetchError] = useState(); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchAssetSavedObjects = async () => { + if ('savedObject' in packageInfo) { + const { + savedObject: { attributes: packageAttributes }, + } = packageInfo; + + if ( + !packageAttributes.installed_kibana || + packageAttributes.installed_kibana.length === 0 + ) { + setIsLoading(false); + return; + } + + try { + const objectsToGet = packageAttributes.installed_kibana.map(({ id, type }) => ({ + id, + type, + })); + const { savedObjects } = await savedObjectsClient.bulkGet(objectsToGet); + setAssetsSavedObjects(savedObjects as AssetSavedObject[]); + } catch (e) { + setFetchError(e); + } finally { + setIsLoading(false); + } + } else { + setIsLoading(false); + } + }; + fetchAssetSavedObjects(); + }, [savedObjectsClient, packageInfo]); + + // if they arrive at this page and the package is not installed, send them to overview + // this happens if they arrive with a direct url or they uninstall while on this tab + if (packageInstallStatus.status !== InstallStatus.installed) { + return ( + + ); + } + + let content: JSX.Element | Array; + + if (isLoading) { + content = ; + } else if (fetchError) { + content = ( + + } + error={fetchError} + /> + ); + } else if (assetSavedObjects === undefined) { + content = ( + +

+ +

+
+ ); + } else { + content = allowedAssetTypes.map((assetType) => { + const sectionAssetSavedObjects = assetSavedObjects.filter((so) => so.type === assetType); + + if (!sectionAssetSavedObjects.length) { + return null; + } + + return ( + <> + + + + ); + }); + } + + return ( + + + {content} + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx new file mode 100644 index 00000000000000..abfdd88d271622 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { FunctionComponent } from 'react'; + +import { + EuiAccordion, + EuiFlexGroup, + EuiFlexItem, + EuiSplitPanel, + EuiSpacer, + EuiText, + EuiLink, + EuiHorizontalRule, + EuiNotificationBadge, +} from '@elastic/eui'; + +import { AssetTitleMap } from '../../../../../constants'; + +import { getHrefToObjectInKibanaApp, useStartServices } from '../../../../../hooks'; + +import type { AllowedAssetType, AssetSavedObject } from './types'; + +interface Props { + type: AllowedAssetType; + savedObjects: AssetSavedObject[]; +} + +export const AssetsAccordion: FunctionComponent = ({ savedObjects, type }) => { + const { http } = useStartServices(); + return ( + + + +

{AssetTitleMap[type]}

+
+
+ + +

{savedObjects.length}

+
+
+
+ } + id={type} + > + <> + + + {savedObjects.map(({ id, attributes: { title, description } }, idx) => { + const pathToObjectInApp = getHrefToObjectInKibanaApp({ + http, + id, + type, + }); + return ( + <> + + +

+ {pathToObjectInApp ? ( + {title} + ) : ( + title + )} +

+
+ {description && ( + <> + + +

{description}

+
+ + )} +
+ {idx + 1 < savedObjects.length && } + + ); + })} +
+ + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/constants.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/constants.ts new file mode 100644 index 00000000000000..d6d88f7935eb44 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/constants.ts @@ -0,0 +1,16 @@ +/* + * 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 { KibanaAssetType } from '../../../../../types'; + +import type { AllowedAssetTypes } from './types'; + +export const allowedAssetTypes: AllowedAssetTypes = [ + KibanaAssetType.dashboard, + KibanaAssetType.search, + KibanaAssetType.visualization, +]; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/index.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/index.ts new file mode 100644 index 00000000000000..ceb030b7ce02eb --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ +export { AssetsPage } from './assets'; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/types.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/types.ts new file mode 100644 index 00000000000000..21efd1cd562e80 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/types.ts @@ -0,0 +1,20 @@ +/* + * 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 { SimpleSavedObject } from 'src/core/public'; + +import type { KibanaAssetType } from '../../../../../types'; + +export type AssetSavedObject = SimpleSavedObject<{ title: string; description?: string }>; + +export type AllowedAssetTypes = [ + KibanaAssetType.dashboard, + KibanaAssetType.search, + KibanaAssetType.visualization +]; + +export type AllowedAssetType = AllowedAssetTypes[number]; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx index 99a29a8194f9b0..cf6007026afebc 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx @@ -56,6 +56,7 @@ import { WithHeaderLayout } from '../../../../layouts'; import { RELEASE_BADGE_DESCRIPTION, RELEASE_BADGE_LABEL } from '../../components/release_badge'; import { IntegrationAgentPolicyCount, UpdateIcon, IconPanel, LoadingIconPanel } from './components'; +import { AssetsPage } from './assets'; import { OverviewPage } from './overview'; import { PackagePoliciesPage } from './policies'; import { SettingsPage } from './settings'; @@ -408,6 +409,24 @@ export function Detail() { }); } + if (packageInstallStatus === InstallStatus.installed && packageInfo.assets) { + tabs.push({ + id: 'assets', + name: ( + + ), + isSelected: panel === 'assets', + 'data-test-subj': `tab-assets`, + href: getHref('integration_details_assets', { + pkgkey: packageInfoKey, + ...(integration ? { integration } : {}), + }), + }); + } + tabs.push({ id: 'settings', name: ( @@ -476,6 +495,9 @@ export function Detail() { + + + diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx index 7da7328fdebbca..c672abeb1c9036 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx @@ -4,9 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { stringify, parse } from 'query-string'; import React, { memo, useCallback, useMemo, useState } from 'react'; -import { Redirect } from 'react-router-dom'; +import { Redirect, useLocation, useHistory } from 'react-router-dom'; import type { CriteriaWithPagination, EuiTableFieldDataColumnType } from '@elastic/eui'; import { EuiButtonIcon, @@ -15,6 +15,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiToolTip, + EuiText, + EuiButton, + EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedRelative, FormattedMessage } from '@kbn/i18n/react'; @@ -66,8 +69,16 @@ interface PackagePoliciesPanelProps { version: string; } export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps) => { - const [flyoutOpenForPolicyId, setFlyoutOpenForPolicyId] = useState(null); - const { getPath } = useLink(); + const { search } = useLocation(); + const history = useHistory(); + const queryParams = useMemo(() => new URLSearchParams(search), [search]); + const agentPolicyIdFromParams = useMemo(() => queryParams.get('addAgentToPolicyId'), [ + queryParams, + ]); + const [flyoutOpenForPolicyId, setFlyoutOpenForPolicyId] = useState( + agentPolicyIdFromParams + ); + const { getPath, getHref } = useLink(); const getPackageInstallStatus = useGetPackageInstallStatus(); const packageInstallStatus = getPackageInstallStatus(name); const { pagination, pageSizeOptions, setPagination } = useUrlPagination(); @@ -87,6 +98,36 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps [setPagination] ); + const renderViewDataStepContent = useCallback( + () => ( + <> + + + {i18n.translate( + 'xpack.fleet.epm.agentEnrollment.viewDataDescription.pleaseNoteLabel', + { defaultMessage: 'Please note' } + )} + + ), + }} + /> + + + + {i18n.translate('xpack.fleet.epm.agentEnrollment.viewDataAssetsLabel', { + defaultMessage: 'View assets', + })} + + + ), + [name, version, getHref] + ); + const columns: Array> = useMemo( () => [ { @@ -186,12 +227,16 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps align: 'right', render({ agentPolicy, packagePolicy }) { return ( - + ); }, }, ], - [] + [renderViewDataStepContent] ); const noItemsMessage = useMemo(() => { @@ -236,14 +281,18 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps /> - {flyoutOpenForPolicyId && ( + {flyoutOpenForPolicyId && !isLoading && ( setFlyoutOpenForPolicyId(null)} - agentPolicies={ - data?.items - .filter(({ agentPolicy }) => agentPolicy.id === flyoutOpenForPolicyId) - .map(({ agentPolicy }) => agentPolicy) ?? [] + onClose={() => { + setFlyoutOpenForPolicyId(null); + const { addAgentToPolicyId, ...rest } = parse(search); + history.replace({ search: stringify(rest) }); + }} + agentPolicy={ + data?.items.find(({ agentPolicy }) => agentPolicy.id === flyoutOpenForPolicyId) + ?.agentPolicy } + viewDataStepContent={renderViewDataStepContent()} /> )} diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts index f1055e7e2583ee..fcf10785664986 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts @@ -37,6 +37,7 @@ jest.mock('./steps', () => { ...module, AgentPolicySelectionStep: jest.fn(), AgentEnrollmentKeySelectionStep: jest.fn(), + ViewDataStep: jest.fn(), }; }); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx index db9245b11b0f99..65118044e98c56 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx @@ -21,7 +21,7 @@ import { FleetStatusProvider, ConfigContext } from '../../hooks'; import { useFleetServerInstructions } from '../../applications/fleet/sections/agents/agent_requirements_page'; -import { AgentEnrollmentKeySelectionStep, AgentPolicySelectionStep } from './steps'; +import { AgentEnrollmentKeySelectionStep, AgentPolicySelectionStep, ViewDataStep } from './steps'; import type { Props } from '.'; import { AgentEnrollmentFlyout } from '.'; @@ -128,6 +128,46 @@ describe('', () => { expect(AgentEnrollmentKeySelectionStep).toHaveBeenCalled(); }); }); + + describe('"View data" extension point', () => { + it('calls the "View data" step when UI extension is provided', async () => { + jest.clearAllMocks(); + await act(async () => { + testBed = await setup({ + agentPolicies: [], + onClose: jest.fn(), + viewDataStepContent:
, + }); + testBed.component.update(); + }); + const { exists, actions } = testBed; + expect(exists('agentEnrollmentFlyout')).toBe(true); + expect(ViewDataStep).toHaveBeenCalled(); + + jest.clearAllMocks(); + actions.goToStandaloneTab(); + expect(ViewDataStep).not.toHaveBeenCalled(); + }); + + it('does not call the "View data" step when UI extension is not provided', async () => { + jest.clearAllMocks(); + await act(async () => { + testBed = await setup({ + agentPolicies: [], + onClose: jest.fn(), + viewDataStepContent: undefined, + }); + testBed.component.update(); + }); + const { exists, actions } = testBed; + expect(exists('agentEnrollmentFlyout')).toBe(true); + expect(ViewDataStep).not.toHaveBeenCalled(); + + jest.clearAllMocks(); + actions.goToStandaloneTab(); + expect(ViewDataStep).not.toHaveBeenCalled(); + }); + }); }); describe('standalone instructions', () => { diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx index b91af80691033e..58362d85e2fb32 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx @@ -42,6 +42,7 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ onClose, agentPolicy, agentPolicies, + viewDataStepContent, }) => { const [mode, setMode] = useState<'managed' | 'standalone'>('managed'); @@ -109,9 +110,17 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ } > {fleetServerHosts.length === 0 && mode === 'managed' ? null : mode === 'managed' ? ( - + ) : ( - + )} diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx index e7045173f1257e..919f0c3052db91 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx @@ -21,7 +21,12 @@ import { useFleetServerInstructions, } from '../../applications/fleet/sections/agents/agent_requirements_page'; -import { DownloadStep, AgentPolicySelectionStep, AgentEnrollmentKeySelectionStep } from './steps'; +import { + DownloadStep, + AgentPolicySelectionStep, + AgentEnrollmentKeySelectionStep, + ViewDataStep, +} from './steps'; import type { BaseProps } from './types'; type Props = BaseProps; @@ -53,83 +58,91 @@ const FleetServerMissingRequirements = () => { return ; }; -export const ManagedInstructions = React.memo(({ agentPolicy, agentPolicies }) => { - const fleetStatus = useFleetStatus(); +export const ManagedInstructions = React.memo( + ({ agentPolicy, agentPolicies, viewDataStepContent }) => { + const fleetStatus = useFleetStatus(); - const [selectedAPIKeyId, setSelectedAPIKeyId] = useState(); - const [isFleetServerPolicySelected, setIsFleetServerPolicySelected] = useState(false); + const [selectedAPIKeyId, setSelectedAPIKeyId] = useState(); + const [isFleetServerPolicySelected, setIsFleetServerPolicySelected] = useState(false); - const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId); - const settings = useGetSettings(); - const fleetServerInstructions = useFleetServerInstructions(apiKey?.data?.item?.policy_id); + const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId); + const settings = useGetSettings(); + const fleetServerInstructions = useFleetServerInstructions(apiKey?.data?.item?.policy_id); - const steps = useMemo(() => { - const { - serviceToken, - getServiceToken, - isLoadingServiceToken, - installCommand, - platform, - setPlatform, - } = fleetServerInstructions; - const fleetServerHosts = settings.data?.item?.fleet_server_hosts || []; - const baseSteps: EuiContainedStepProps[] = [ - DownloadStep(), - !agentPolicy - ? AgentPolicySelectionStep({ - agentPolicies, - setSelectedAPIKeyId, - setIsFleetServerPolicySelected, - }) - : AgentEnrollmentKeySelectionStep({ agentPolicy, setSelectedAPIKeyId }), - ]; - if (isFleetServerPolicySelected) { - baseSteps.push( - ...[ - ServiceTokenStep({ serviceToken, getServiceToken, isLoadingServiceToken }), - FleetServerCommandStep({ serviceToken, installCommand, platform, setPlatform }), - ] - ); - } else { - baseSteps.push({ - title: i18n.translate('xpack.fleet.agentEnrollment.stepEnrollAndRunAgentTitle', { - defaultMessage: 'Enroll and start the Elastic Agent', - }), - children: selectedAPIKeyId && apiKey.data && ( - - ), - }); - } - return baseSteps; - }, [ - agentPolicy, - agentPolicies, - selectedAPIKeyId, - apiKey.data, - isFleetServerPolicySelected, - settings.data?.item?.fleet_server_hosts, - fleetServerInstructions, - ]); + const steps = useMemo(() => { + const { + serviceToken, + getServiceToken, + isLoadingServiceToken, + installCommand, + platform, + setPlatform, + } = fleetServerInstructions; + const fleetServerHosts = settings.data?.item?.fleet_server_hosts || []; + const baseSteps: EuiContainedStepProps[] = [ + DownloadStep(), + !agentPolicy + ? AgentPolicySelectionStep({ + agentPolicies, + setSelectedAPIKeyId, + setIsFleetServerPolicySelected, + }) + : AgentEnrollmentKeySelectionStep({ agentPolicy, setSelectedAPIKeyId }), + ]; + if (isFleetServerPolicySelected) { + baseSteps.push( + ...[ + ServiceTokenStep({ serviceToken, getServiceToken, isLoadingServiceToken }), + FleetServerCommandStep({ serviceToken, installCommand, platform, setPlatform }), + ] + ); + } else { + baseSteps.push({ + title: i18n.translate('xpack.fleet.agentEnrollment.stepEnrollAndRunAgentTitle', { + defaultMessage: 'Enroll and start the Elastic Agent', + }), + children: selectedAPIKeyId && apiKey.data && ( + + ), + }); + } - return ( - <> - {fleetStatus.isReady ? ( - <> - - - - - - - ) : fleetStatus.missingRequirements?.length === 1 && - fleetStatus.missingRequirements[0] === 'fleet_server' ? ( - - ) : ( - - )} - - ); -}); + if (viewDataStepContent) { + baseSteps.push(ViewDataStep(viewDataStepContent)); + } + + return baseSteps; + }, [ + agentPolicy, + agentPolicies, + selectedAPIKeyId, + apiKey.data, + isFleetServerPolicySelected, + settings.data?.item?.fleet_server_hosts, + fleetServerInstructions, + viewDataStepContent, + ]); + + return ( + <> + {fleetStatus.isReady ? ( + <> + + + + + + + ) : fleetStatus.missingRequirements?.length === 1 && + fleetStatus.missingRequirements[0] === 'fleet_server' ? ( + + ) : ( + + )} + + ); + } +); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx index ea4fa626afbb6a..03cff88e639695 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx @@ -138,3 +138,16 @@ export const AgentEnrollmentKeySelectionStep = ({ ), }; }; + +/** + * Send users to assets installed by the package in Kibana so they can + * view their data. + */ +export const ViewDataStep = (content: JSX.Element) => { + return { + title: i18n.translate('xpack.fleet.agentEnrollment.stepViewDataTitle', { + defaultMessage: 'View your data', + }), + children: content, + }; +}; diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts index b9bcf8fb3e4b27..e0c5b040a61fb2 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts @@ -9,12 +9,20 @@ import type { AgentPolicy } from '../../types'; export interface BaseProps { /** - * The user selected policy to be used + * The user selected policy to be used. If this value is `undefined` a value must be provided for `agentPolicies`. */ agentPolicy?: AgentPolicy; /** - * A selection of policies for the user to choose from, will be ignored if `agentPolicy` has been provided + * A selection of policies for the user to choose from, will be ignored if `agentPolicy` has been provided. + * + * If this value is `undefined` a value must be provided for `agentPolicy`. */ agentPolicies?: AgentPolicy[]; + + /** + * There is a step in the agent enrollment process that allows users to see the data from an integration represented in the UI + * in some way. This is an area for consumers to render a button and text explaining how data can be viewed. + */ + viewDataStepContent?: JSX.Element; } diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx index 03bf2095f7f3e0..1f64de27fce392 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx @@ -21,7 +21,8 @@ import { PackagePolicyDeleteProvider } from './package_policy_delete_provider'; export const PackagePolicyActionsMenu: React.FunctionComponent<{ agentPolicy: AgentPolicy; packagePolicy: PackagePolicy; -}> = ({ agentPolicy, packagePolicy }) => { + viewDataStepContent?: JSX.Element; +}> = ({ agentPolicy, packagePolicy, viewDataStepContent }) => { const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); const { getHref } = useLink(); const hasWriteCapabilities = useCapabilities().write; @@ -103,7 +104,11 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ <> {isEnrollmentFlyoutOpen && ( - + )} diff --git a/x-pack/plugins/fleet/public/constants/page_paths.ts b/x-pack/plugins/fleet/public/constants/page_paths.ts index 326cfd804bd570..1688a396cd5a15 100644 --- a/x-pack/plugins/fleet/public/constants/page_paths.ts +++ b/x-pack/plugins/fleet/public/constants/page_paths.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { stringify } from 'query-string'; + export type StaticPage = | 'base' | 'overview' @@ -19,6 +21,7 @@ export type StaticPage = export type DynamicPage = | 'integration_details_overview' | 'integration_details_policies' + | 'integration_details_assets' | 'integration_details_settings' | 'integration_details_custom' | 'integration_policy_edit' @@ -66,6 +69,7 @@ export const INTEGRATIONS_ROUTING_PATHS = { integration_details: '/detail/:pkgkey/:panel?', integration_details_overview: '/detail/:pkgkey/overview', integration_details_policies: '/detail/:pkgkey/policies', + integration_details_assets: '/detail/:pkgkey/assets', integration_details_settings: '/detail/:pkgkey/settings', integration_details_custom: '/detail/:pkgkey/custom', integration_policy_edit: '/edit-integration/:packagePolicyId', @@ -86,9 +90,13 @@ export const pagePathGetters: { INTEGRATIONS_BASE_PATH, `/detail/${pkgkey}/overview${integration ? `?integration=${integration}` : ''}`, ], - integration_details_policies: ({ pkgkey, integration }) => [ + integration_details_policies: ({ pkgkey, integration, addAgentToPolicyId }) => { + const qs = stringify({ integration, addAgentToPolicyId }); + return [INTEGRATIONS_BASE_PATH, `/detail/${pkgkey}/policies${qs ? `?${qs}` : ''}`]; + }, + integration_details_assets: ({ pkgkey, integration }) => [ INTEGRATIONS_BASE_PATH, - `/detail/${pkgkey}/policies${integration ? `?integration=${integration}` : ''}`, + `/detail/${pkgkey}/assets${integration ? `?integration=${integration}` : ''}`, ], integration_details_settings: ({ pkgkey, integration }) => [ INTEGRATIONS_BASE_PATH, @@ -108,6 +116,7 @@ export const pagePathGetters: { FLEET_BASE_PATH, `/policies/${policyId}${tabId ? `/${tabId}` : ''}`, ], + // TODO: This might need to be removed because we do not have a way to pick an integration in line anymore add_integration_from_policy: ({ policyId }) => [ FLEET_BASE_PATH, `/policies/${policyId}/add-integration`, diff --git a/x-pack/plugins/fleet/public/hooks/index.ts b/x-pack/plugins/fleet/public/hooks/index.ts index 9f41e5c7cc92b5..a00c0c5dacf111 100644 --- a/x-pack/plugins/fleet/public/hooks/index.ts +++ b/x-pack/plugins/fleet/public/hooks/index.ts @@ -11,7 +11,7 @@ export { useConfig, ConfigContext } from './use_config'; export { useKibanaVersion, KibanaVersionContext } from './use_kibana_version'; export { licenseService, useLicense } from './use_license'; export { useLink } from './use_link'; -export { useKibanaLink } from './use_kibana_link'; +export { useKibanaLink, getHrefToObjectInKibanaApp } from './use_kibana_link'; export { usePackageIconType, UsePackageIconType } from './use_package_icon_type'; export { usePagination, Pagination, PAGE_SIZE_OPTIONS } from './use_pagination'; export { useUrlPagination } from './use_url_pagination'; diff --git a/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts b/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts index 29f4f8748d1a0e..3ad01620b9780f 100644 --- a/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts +++ b/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts @@ -4,12 +4,62 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { HttpStart } from 'src/core/public'; + +import { KibanaAssetType } from '../types'; import { useStartServices } from './'; const KIBANA_BASE_PATH = '/app/kibana'; +const getKibanaLink = (http: HttpStart, path: string) => { + return http.basePath.prepend(`${KIBANA_BASE_PATH}#${path}`); +}; + +/** + * TODO: This is a temporary solution for getting links to various assets. It is very risky because: + * + * 1. The plugin might not exist/be enabled + * 2. URLs and paths might not always be supported + * + * We should migrate to using the new URL service locators. + * + * @deprecated {@link Locators} from the new URL service need to be used instead. + + */ +export const getHrefToObjectInKibanaApp = ({ + type, + id, + http, +}: { + type: KibanaAssetType; + id: string; + http: HttpStart; +}): undefined | string => { + let kibanaAppPath: undefined | string; + switch (type) { + case KibanaAssetType.dashboard: + kibanaAppPath = `/dashboard/${id}`; + break; + case KibanaAssetType.search: + kibanaAppPath = `/discover/${id}`; + break; + case KibanaAssetType.visualization: + kibanaAppPath = `/visualize/edit/${id}`; + break; + default: + return undefined; + } + + return getKibanaLink(http, kibanaAppPath); +}; + +/** + * TODO: This functionality needs to be replaced with use of the new URL service locators + * + * @deprecated {@link Locators} from the new URL service need to be used instead. + */ export function useKibanaLink(path: string = '/') { - const core = useStartServices(); - return core.http.basePath.prepend(`${KIBANA_BASE_PATH}#${path}`); + const { http } = useStartServices(); + return getKibanaLink(http, path); }