diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/components/back_link.test.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/components/back_link.test.tsx index 613970fc24085..ce8f4e5fd5355 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/components/back_link.test.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/components/back_link.test.tsx @@ -5,60 +5,68 @@ * 2.0. */ -import { render } from '@testing-library/react'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; import React from 'react'; import { I18nProvider } from '@kbn/i18n-react'; +import { useStartServices } from '../../../../../../../hooks'; + import { BackLink } from './back_link'; +jest.mock('../../../../../../../hooks', () => { + return { + ...jest.requireActual('../../../../../../../hooks'), + useStartServices: jest.fn().mockReturnValue({ + application: { navigateToApp: jest.fn() }, + }), + }; +}); + describe('BackLink', () => { - it('renders back to selection link', () => { - const expectedUrl = '/app/experimental-onboarding'; - const queryParams = new URLSearchParams(); - queryParams.set('observabilityOnboardingLink', expectedUrl); - const { getByText, getByRole } = render( - - - - ); - expect(getByText('Back to selection')).toBeInTheDocument(); - expect(getByRole('link').getAttribute('href')).toBe(expectedUrl); + beforeEach(() => { + jest.mocked(useStartServices().application.navigateToApp).mockReset(); }); - it('renders back to selection link when onboardingLink param is provided', () => { - const expectedUrl = '/app/experimental-onboarding'; + it('renders back to selection link when returnAppId and returnPath are present', async () => { + const appId = 'observabilityOnboarding'; + const path = '?category=aws'; const queryParams = new URLSearchParams(); - queryParams.set('onboardingLink', expectedUrl); - const { getByText, getByRole } = render( - - - - ); - expect(getByText('Back to selection')).toBeInTheDocument(); - expect(getByRole('link').getAttribute('href')).toBe(expectedUrl); - }); + queryParams.set('returnAppId', appId); + queryParams.set('returnPath', path); - it('renders back to selection link with params', () => { - const expectedUrl = '/app/experimental-onboarding&search=aws&category=infra'; - const queryParams = new URLSearchParams(); - queryParams.set('observabilityOnboardingLink', expectedUrl); - const { getByText, getByRole } = render( + const { getByText } = render( - + ); expect(getByText('Back to selection')).toBeInTheDocument(); - expect(getByRole('link').getAttribute('href')).toBe(expectedUrl); + await act(async () => { + fireEvent.click(getByText('Back to selection')); + }); + await waitFor(() => { + expect(useStartServices().application.navigateToApp).toHaveBeenCalledWith(appId, { + path, + }); + }); }); - it('renders back to integrations link', () => { + it('renders back to integrations link when no query params are present', async () => { + const appId = 'integrations'; + const path = '/browse'; const queryParams = new URLSearchParams(); - const { getByText, getByRole } = render( + const { getByText } = render( - + ); expect(getByText('Back to integrations')).toBeInTheDocument(); - expect(getByRole('link').getAttribute('href')).toBe('/app/integrations'); + await act(async () => { + fireEvent.click(getByText('Back to integrations')); + }); + await waitFor(() => { + expect(useStartServices().application.navigateToApp).toHaveBeenCalledWith(appId, { + path, + }); + }); }); }); diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/components/back_link.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/components/back_link.tsx index 75d3461bdfee6..841f57eeb136e 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/components/back_link.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/components/back_link.tsx @@ -6,29 +6,47 @@ */ import { EuiButtonEmpty } from '@elastic/eui'; + import { FormattedMessage } from '@kbn/i18n-react'; import React, { useMemo } from 'react'; +import { useStartServices } from '../../../../../../../hooks'; + interface Props { queryParams: URLSearchParams; - href: string; + integrationsPath: string; } -export function BackLink({ queryParams, href: integrationsHref }: Props) { - const { onboardingLink } = useMemo(() => { +export function BackLink({ queryParams, integrationsPath }: Props) { + const { + application: { navigateToApp }, + } = useStartServices(); + const { returnAppId, returnPath } = useMemo(() => { return { - onboardingLink: - // Users from Security Solution onboarding page will have onboardingLink to redirect back to the onboarding page - queryParams.get('observabilityOnboardingLink') || queryParams.get('onboardingLink'), + // Check for custom path params to redirect back to a specified app's path + returnAppId: queryParams.get('returnAppId'), + returnPath: queryParams.get('returnPath'), }; }, [queryParams]); - const href = onboardingLink ?? integrationsHref; - const message = onboardingLink ? BACK_TO_SELECTION : BACK_TO_INTEGRATIONS; + + const appId = returnAppId && returnPath ? returnAppId : 'integrations'; + const path = returnAppId && returnPath ? returnPath : integrationsPath; + + const message = returnPath ? BACK_TO_SELECTION : BACK_TO_INTEGRATIONS; return ( - - {message} - + <> + { + navigateToApp(appId, { path }); + }} + > + {message} + + ); } diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx index 428ab28c8db16..ed3bc78b5d5ac 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx @@ -141,11 +141,11 @@ export function Detail() { const queryParams = useMemo(() => new URLSearchParams(search), [search]); const integration = useMemo(() => queryParams.get('integration'), [queryParams]); const prerelease = useMemo(() => Boolean(queryParams.get('prerelease')), [queryParams]); - /** Users from Security Solution onboarding page will have onboardingLink and onboardingAppId in the query params - ** to redirect back to the onboarding page after adding an integration + /** Users from Security and Observability Solution onboarding pages will have returnAppId and returnPath + ** in the query params to redirect back to the onboarding page after adding an integration */ - const onboardingLink = useMemo(() => queryParams.get('onboardingLink'), [queryParams]); - const onboardingAppId = useMemo(() => queryParams.get('onboardingAppId'), [queryParams]); + const returnAppId = useMemo(() => queryParams.get('returnAppId'), [queryParams]); + const returnPath = useMemo(() => queryParams.get('returnPath'), [queryParams]); const authz = useAuthz(); const canAddAgent = authz.fleet.addAgents; @@ -318,12 +318,12 @@ export function Detail() { const fromIntegrations = getFromIntegrations(); - const href = + const fromIntegrationsPath = fromIntegrations === 'updates_available' - ? getHref('integrations_installed_updates_available') + ? getPath('integrations_installed_updates_available') : fromIntegrations === 'installed' - ? getHref('integrations_installed') - : getHref('integrations_all'); + ? getPath('integrations_installed') + : getPath('integrations_all'); const numOfDeferredInstallations = useMemo( () => getDeferredInstallationsCnt(packageInfo), @@ -336,7 +336,7 @@ export function Detail() { {/* Allows button to break out of full width */}
- +
@@ -393,7 +393,7 @@ export function Detail() { ), - [integrationInfo, isLoading, packageInfo, href, queryParams] + [integrationInfo, isLoading, packageInfo, fromIntegrationsPath, queryParams] ); const handleAddIntegrationPolicyClick = useCallback( @@ -418,20 +418,20 @@ export function Detail() { isAgentlessIntegration: isAgentlessIntegration(packageInfo || undefined), }); - /** Users from Security Solution onboarding page will have onboardingLink and onboardingAppId in the query params - ** to redirect back to the onboarding page after adding an integration + /** Users from Security and Observability Solution onboarding pages will have returnAppId and returnPath + ** in the query params to redirect back to the onboarding page after adding an integration */ const navigateOptions: InstallPkgRouteOptions = - onboardingAppId && onboardingLink + returnAppId && returnPath ? [ defaultNavigateOptions[0], { ...defaultNavigateOptions[1], state: { ...(defaultNavigateOptions[1]?.state ?? {}), - onCancelNavigateTo: [onboardingAppId, { path: onboardingLink }], - onCancelUrl: onboardingLink, - onSaveNavigateTo: [onboardingAppId, { path: onboardingLink }], + onCancelNavigateTo: [returnAppId, { path: returnPath }], + onCancelUrl: services.application.getUrlForApp(returnAppId, { path: returnPath }), + onSaveNavigateTo: [returnAppId, { path: returnPath }], }, }, ] @@ -448,8 +448,8 @@ export function Detail() { isCloud, isFirstTimeAgentUser, isGuidedOnboardingActive, - onboardingAppId, - onboardingLink, + returnAppId, + returnPath, packageInfo, pathname, pkgkey, diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/package_list_search_form/use_card_url_rewrite.test.ts b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/package_list_search_form/use_card_url_rewrite.test.ts index b0746f798f309..f7b8725c40744 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/package_list_search_form/use_card_url_rewrite.test.ts +++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/package_list_search_form/use_card_url_rewrite.test.ts @@ -5,60 +5,37 @@ * 2.0. */ -import { addPathParamToUrl, toOnboardingPath } from './use_card_url_rewrite'; +import { addPathParamToUrl, buildOnboardingPath } from './use_card_url_rewrite'; describe('useIntegratrionCardList', () => { describe('toOnboardingPath', () => { - it('returns null if no `basePath` is defined', () => { - expect(toOnboardingPath({})).toBeNull(); - }); - it('returns just the `basePath` if no category or search is defined', () => { - expect(toOnboardingPath({ basePath: '' })).toBe('/app/observabilityOnboarding'); - expect(toOnboardingPath({ basePath: '/s/custom_space_name' })).toBe( - '/s/custom_space_name/app/observabilityOnboarding' - ); - }); it('includes category in the URL', () => { - expect(toOnboardingPath({ basePath: '/s/custom_space_name', category: 'logs' })).toBe( - '/s/custom_space_name/app/observabilityOnboarding?category=logs' - ); - expect(toOnboardingPath({ basePath: '', category: 'infra' })).toBe( - '/app/observabilityOnboarding?category=infra' - ); + expect(buildOnboardingPath({ category: 'logs' })).toBe('?category=logs'); }); it('includes search in the URL', () => { - expect(toOnboardingPath({ basePath: '/s/custom_space_name', search: 'search' })).toBe( - '/s/custom_space_name/app/observabilityOnboarding?search=search' - ); + expect(buildOnboardingPath({ search: 'search' })).toBe('?search=search'); }); it('includes category and search in the URL', () => { expect( - toOnboardingPath({ basePath: '/s/custom_space_name', category: 'logs', search: 'search' }) - ).toBe('/s/custom_space_name/app/observabilityOnboarding?category=logs&search=search'); - expect(toOnboardingPath({ basePath: '', category: 'infra', search: 'search' })).toBe( - '/app/observabilityOnboarding?category=infra&search=search' - ); + buildOnboardingPath({ + category: 'logs', + search: 'search', + }) + ).toBe('?category=logs&search=search'); }); }); + describe('addPathParamToUrl', () => { it('adds the onboarding link to url with existing params', () => { expect( - addPathParamToUrl( - '/app/integrations?query-1', - '/app/observabilityOnboarding?search=aws&category=infra' - ) + addPathParamToUrl('/app/integrations?query-1', { search: 'aws', category: 'infra' }) ).toBe( - '/app/integrations?query-1&observabilityOnboardingLink=%2Fapp%2FobservabilityOnboarding%3Fsearch%3Daws%26category%3Dinfra' + '/app/integrations?query-1&returnAppId=observabilityOnboarding&returnPath=%3Fcategory%3Dinfra%26search%3Daws' ); }); it('adds the onboarding link to url without existing params', () => { - expect( - addPathParamToUrl( - '/app/integrations', - '/app/experimental-onboarding?search=aws&category=infra' - ) - ).toBe( - '/app/integrations?observabilityOnboardingLink=%2Fapp%2Fexperimental-onboarding%3Fsearch%3Daws%26category%3Dinfra' + expect(addPathParamToUrl('/app/integrations', {})).toBe( + '/app/integrations?returnAppId=observabilityOnboarding&returnPath=%3F' ); }); }); diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/package_list_search_form/use_card_url_rewrite.ts b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/package_list_search_form/use_card_url_rewrite.ts index ce462566bc4d1..63faad1253a84 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/package_list_search_form/use_card_url_rewrite.ts +++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/package_list_search_form/use_card_url_rewrite.ts @@ -5,45 +5,50 @@ * 2.0. */ -import { useMemo } from 'react'; import { IntegrationCardItem } from '@kbn/fleet-plugin/public'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { OBSERVABILITY_ONBOARDING_APP_ID } from '@kbn/deeplinks-observability'; -export function toOnboardingPath({ - basePath, +export function buildOnboardingPath({ category, search, }: { - basePath?: string; category?: string | null; search?: string; -}): string | null { - if (typeof basePath !== 'string' && !basePath) return null; - const path = `${basePath}/app/observabilityOnboarding`; - if (!category && !search) return path; +}): string { + if (!category && !search) return '?'; const params = new URLSearchParams(); if (category) params.append('category', category); if (search) params.append('search', search); - return `${path}?${params.toString()}`; + return `?${params.toString()}`; } -export function addPathParamToUrl(url: string, onboardingLink: string) { - const encoded = encodeURIComponent(onboardingLink); +export function addPathParamToUrl( + url: string, + params: { + category?: string | null; + search?: string; + } +) { + const onboardingPath = buildOnboardingPath(params); + const encoded = encodeURIComponent(onboardingPath); + const paramsString = `returnAppId=${OBSERVABILITY_ONBOARDING_APP_ID}&returnPath=${encoded}`; + if (url.indexOf('?') >= 0) { - return `${url}&observabilityOnboardingLink=${encoded}`; + return `${url}&${paramsString}`; } - return `${url}?observabilityOnboardingLink=${encoded}`; + return `${url}?${paramsString}`; } export function useCardUrlRewrite(props: { category?: string | null; search?: string }) { - const kibana = useKibana(); - const basePath = kibana.services.http?.basePath.get(); - const onboardingLink = useMemo(() => toOnboardingPath({ basePath, ...props }), [basePath, props]); + const params = new URLSearchParams(); + if (props.category) params.append('category', props.category); + if (props.search) params.append('search', props.search); + return (card: IntegrationCardItem) => ({ ...card, url: - card.url.indexOf('/app/integrations') >= 0 && onboardingLink - ? addPathParamToUrl(card.url, onboardingLink) + card.url.indexOf('/app/integrations') >= 0 + ? addPathParamToUrl(card.url, { category: props.category, search: props.search }) : card.url, }); } diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/constants.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/constants.ts index c748f5205e7aa..f8d60a11481ca 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/constants.ts @@ -18,8 +18,8 @@ export const FLEET_APP_ID = `fleet`; export const INTEGRATION_APP_ID = `integrations`; export const LOADING_SKELETON_TEXT_LINES = 10; // 10 lines of text export const MAX_CARD_HEIGHT_IN_PX = 127; // px -export const ONBOARDING_APP_ID = 'onboardingAppId'; -export const ONBOARDING_LINK = 'onboardingLink'; +export const RETURN_APP_ID = 'returnAppId'; +export const RETURN_PATH = 'returnPath'; export const SCROLL_ELEMENT_ID = 'integrations-scroll-container'; export const SEARCH_FILTER_CATEGORIES: CategoryFacet[] = []; export const WITH_SEARCH_BOX_HEIGHT = '568px'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts index 660464ba73501..4a4eb99689c3d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts @@ -18,8 +18,8 @@ import { CARD_TITLE_LINE_CLAMP, INTEGRATION_APP_ID, MAX_CARD_HEIGHT_IN_PX, - ONBOARDING_APP_ID, - ONBOARDING_LINK, + RETURN_APP_ID, + RETURN_PATH, TELEMETRY_INTEGRATION_CARD, } from './constants'; import type { GetAppUrl, NavigateTo } from '../../../../../common/lib/kibana'; @@ -27,7 +27,7 @@ import { trackOnboardingLinkClick } from '../../../lib/telemetry'; const addPathParamToUrl = (url: string, onboardingLink: string) => { const encoded = encodeURIComponent(onboardingLink); - const paramsString = `${ONBOARDING_LINK}=${encoded}&${ONBOARDING_APP_ID}=${APP_UI_ID}`; + const paramsString = `${RETURN_PATH}=${encoded}&${RETURN_APP_ID}=${APP_UI_ID}`; if (url.indexOf('?') >= 0) { return `${url}&${paramsString}`; @@ -89,7 +89,7 @@ const addSecuritySpecificProps = ({ }; const url = card.url.indexOf(APP_INTEGRATIONS_PATH) >= 0 && onboardingLink - ? addPathParamToUrl(card.url, onboardingLink) + ? addPathParamToUrl(card.url, ONBOARDING_PATH) : card.url; return { ...card,