diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx index bf03648f442c9..474ffe2e4db70 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx @@ -16,6 +16,7 @@ import { EuiSpacer, EuiToolTip, } from '@elastic/eui'; +import { css } from '@emotion/react'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; @@ -175,6 +176,18 @@ export function PackageCard({ > { + 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); + }); + + 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( + + ); + expect(getByText('Back to selection')).toBeInTheDocument(); + expect(getByRole('link').getAttribute('href')).toBe(expectedUrl); + }); + + it('renders back to integrations link', () => { + const queryParams = new URLSearchParams(); + const { getByText, getByRole } = render( + + ); + expect(getByText('Back to integrations')).toBeInTheDocument(); + expect(getByRole('link').getAttribute('href')).toBe('/app/integrations'); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/back_link.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/back_link.tsx new file mode 100644 index 0000000000000..081b78de8ec51 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/back_link.tsx @@ -0,0 +1,45 @@ +/* + * 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 { EuiButtonEmpty } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useMemo } from 'react'; + +interface Props { + queryParams: URLSearchParams; + href: string; +} + +export function BackLink({ queryParams, href: integrationsHref }: Props) { + const { onboardingLink } = useMemo(() => { + return { + onboardingLink: queryParams.get('observabilityOnboardingLink'), + }; + }, [queryParams]); + const href = onboardingLink ?? integrationsHref; + const message = onboardingLink ? BACK_TO_SELECTION : BACK_TO_INTEGRATIONS; + + return ( + + {message} + + ); +} + +const BACK_TO_INTEGRATIONS = ( + +); + +const BACK_TO_SELECTION = ( + +); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/index.tsx index c0438bf6dfe8d..4aa1a543897c9 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/index.tsx @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +export { BackLink } from './back_link'; export { AddIntegrationButton } from './add_integration_button'; export { UpdateIcon } from './update_icon'; export { IntegrationAgentPolicyCount } from './integration_agent_policy_count'; 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 54ca058865fdf..0a2fc69a69366 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 @@ -12,7 +12,6 @@ import { Routes, Route } from '@kbn/shared-ux-router'; import styled from 'styled-components'; import { EuiBadge, - EuiButtonEmpty, EuiCallOut, EuiDescriptionList, EuiDescriptionListDescription, @@ -71,6 +70,7 @@ import { DeferredAssetsWarning } from './assets/deferred_assets_warning'; import { useIsFirstTimeAgentUserQuery } from './hooks'; import { getInstallPkgRouteOptions } from './utils'; import { + BackLink, IntegrationAgentPolicyCount, UpdateIcon, IconPanel, @@ -314,12 +314,7 @@ export function Detail() { {/* Allows button to break out of full width */}
- - - +
@@ -366,7 +361,7 @@ export function Detail() { ), - [integrationInfo, isLoading, packageInfo, href] + [integrationInfo, isLoading, packageInfo, href, queryParams] ); const handleAddIntegrationPolicyClick = useCallback( diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx index 6a1a062e5a7e1..e5977ff0172c7 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import type { FunctionComponent } from 'react'; import { @@ -81,8 +81,21 @@ export const OnboardingFlowForm: FunctionComponent = () => { const radioGroupId = useGeneratedHtmlId({ prefix: 'onboardingCategory' }); const [searchParams, setSearchParams] = useSearchParams(); + const packageListRef = React.useRef(null); - const [integrationSearch, setIntegrationSearch] = useState(''); + const [integrationSearch, setIntegrationSearch] = useState(searchParams.get('search') ?? ''); + + useEffect(() => { + const searchParam = searchParams.get('search') ?? ''; + if (integrationSearch === searchParam) return; + const entries: Record = Object.fromEntries(searchParams.entries()); + if (integrationSearch) { + entries.search = integrationSearch; + } else { + delete entries.search; + } + setSearchParams(entries, { replace: true }); + }, [integrationSearch, searchParams, setSearchParams]); const createCollectionCardHandler = useCallback( (query: string) => () => { @@ -97,7 +110,7 @@ export const OnboardingFlowForm: FunctionComponent = () => { ); } }, - [setIntegrationSearch] + [] ); const customCards = useCustomCardsForCategory( @@ -153,7 +166,13 @@ export const OnboardingFlowForm: FunctionComponent = () => { /> - {Array.isArray(customCards) && } + {Array.isArray(customCards) && ( + + )} { type === 'generated')} + customCards={customCards?.filter( + (card) => card.type === 'generated' && !card.isCollectionCard + )} joinCardLists /> diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/packages_list/index.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/packages_list/index.tsx index 85627f9411bf3..135e4650ccc09 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/packages_list/index.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/packages_list/index.tsx @@ -32,6 +32,8 @@ interface Props { packageListRef?: React.Ref; searchQuery?: string; setSearchQuery?: React.Dispatch>; + flowCategory?: string | null; + flowSearch?: string; /** * When enabled, custom and integration cards are joined into a single list. */ @@ -52,6 +54,8 @@ const PackageListGridWrapper = ({ searchQuery, setSearchQuery, customCards, + flowCategory, + flowSearch, joinCardLists = false, }: WrapperProps) => { const customMargin = useCustomMargin(); @@ -63,6 +67,8 @@ const PackageListGridWrapper = ({ filteredCards, selectedCategory, customCards, + flowCategory, + flowSearch, joinCardLists ); diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/packages_list/use_integration_card_list.test.ts b/x-pack/plugins/observability_solution/observability_onboarding/public/application/packages_list/use_integration_card_list.test.ts new file mode 100644 index 0000000000000..076f4585e2667 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/packages_list/use_integration_card_list.test.ts @@ -0,0 +1,65 @@ +/* + * 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 { addPathParamToUrl, toOnboardingPath } from './use_integration_card_list'; + +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/experimental-onboarding'); + expect(toOnboardingPath({ basePath: '/s/custom_space_name' })).toBe( + '/s/custom_space_name/app/experimental-onboarding' + ); + }); + it('includes category in the URL', () => { + expect(toOnboardingPath({ basePath: '/s/custom_space_name', category: 'logs' })).toBe( + '/s/custom_space_name/app/experimental-onboarding?category=logs' + ); + expect(toOnboardingPath({ basePath: '', category: 'infra' })).toBe( + '/app/experimental-onboarding?category=infra' + ); + }); + it('includes search in the URL', () => { + expect(toOnboardingPath({ basePath: '/s/custom_space_name', search: 'search' })).toBe( + '/s/custom_space_name/app/experimental-onboarding?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/experimental-onboarding?category=logs&search=search'); + expect(toOnboardingPath({ basePath: '', category: 'infra', search: 'search' })).toBe( + '/app/experimental-onboarding?category=infra&search=search' + ); + }); + }); + describe('addPathParamToUrl', () => { + it('adds the onboarding link to url with existing params', () => { + expect( + addPathParamToUrl( + '/app/integrations?query-1', + '/app/experimental-onboarding?search=aws&category=infra' + ) + ).toBe( + '/app/integrations?query-1&observabilityOnboardingLink=%2Fapp%2Fexperimental-onboarding%3Fsearch%3Daws%26category%3Dinfra' + ); + }); + 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' + ); + }); + }); +}); diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/packages_list/use_integration_card_list.ts b/x-pack/plugins/observability_solution/observability_onboarding/public/application/packages_list/use_integration_card_list.ts index 8d5a275a4523b..4d52856ce5551 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/packages_list/use_integration_card_list.ts +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/packages_list/use_integration_card_list.ts @@ -7,9 +7,50 @@ import { useMemo } from 'react'; import { IntegrationCardItem } from '@kbn/fleet-plugin/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; import { CustomCard } from './types'; +import { EXPERIMENTAL_ONBOARDING_APP_ROUTE } from '../../common'; import { toCustomCard } from './utils'; +export function toOnboardingPath({ + basePath, + category, + search, +}: { + basePath?: string; + category?: string | null; + search?: string; +}): string | null { + if (typeof basePath !== 'string' && !basePath) return null; + const path = `${basePath}${EXPERIMENTAL_ONBOARDING_APP_ROUTE}`; + if (!category && !search) return path; + const params = new URLSearchParams(); + if (category) params.append('category', category); + if (search) params.append('search', search); + return `${path}?${params.toString()}`; +} + +export function addPathParamToUrl(url: string, onboardingLink: string) { + const encoded = encodeURIComponent(onboardingLink); + if (url.indexOf('?') >= 0) { + return `${url}&observabilityOnboardingLink=${encoded}`; + } + return `${url}?observabilityOnboardingLink=${encoded}`; +} + +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]); + return (card: IntegrationCardItem) => ({ + ...card, + url: + card.url.indexOf('/app/integrations') >= 0 && onboardingLink + ? addPathParamToUrl(card.url, onboardingLink) + : card.url, + }); +} + function extractFeaturedCards(filteredCards: IntegrationCardItem[], featuredCardNames?: string[]) { const featuredCards: Record = {}; filteredCards.forEach((card) => { @@ -21,21 +62,23 @@ function extractFeaturedCards(filteredCards: IntegrationCardItem[], featuredCard } function formatCustomCards( + rewriteUrl: (card: IntegrationCardItem) => IntegrationCardItem, customCards: CustomCard[], featuredCards: Record ) { const cards: IntegrationCardItem[] = []; for (const card of customCards) { if (card.type === 'featured' && !!featuredCards[card.name]) { - cards.push(toCustomCard(featuredCards[card.name]!)); + cards.push(toCustomCard(rewriteUrl(featuredCards[card.name]!))); } else if (card.type === 'generated') { - cards.push(toCustomCard(card)); + cards.push(toCustomCard(rewriteUrl(card))); } } return cards; } function useFilteredCards( + rewriteUrl: (card: IntegrationCardItem) => IntegrationCardItem, integrationsList: IntegrationCardItem[], selectedCategory: string, customCards?: CustomCard[] @@ -43,6 +86,7 @@ function useFilteredCards( return useMemo(() => { const integrationCards = integrationsList .filter((card) => card.categories.includes(selectedCategory)) + .map(rewriteUrl) .map(toCustomCard); if (!customCards) { @@ -56,7 +100,7 @@ function useFilteredCards( ), integrationCards, }; - }, [integrationsList, customCards, selectedCategory]); + }, [integrationsList, customCards, selectedCategory, rewriteUrl]); } /** @@ -71,16 +115,20 @@ export function useIntegrationCardList( integrationsList: IntegrationCardItem[], selectedCategory = 'observability', customCards?: CustomCard[], + flowCategory?: string | null, + flowSearch?: string, fullList = false ): IntegrationCardItem[] { + const rewriteUrl = useCardUrlRewrite({ category: flowCategory, search: flowSearch }); const { featuredCards, integrationCards } = useFilteredCards( + rewriteUrl, integrationsList, selectedCategory, customCards ); if (customCards && customCards.length > 0) { - const formattedCustomCards = formatCustomCards(customCards, featuredCards); + const formattedCustomCards = formatCustomCards(rewriteUrl, customCards, featuredCards); if (fullList) { return [...formattedCustomCards, ...integrationCards] as IntegrationCardItem[]; } diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/common.ts b/x-pack/plugins/observability_solution/observability_onboarding/public/common.ts new file mode 100644 index 0000000000000..110aeeebcd1c6 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/common.ts @@ -0,0 +1,8 @@ +/* + * 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 const EXPERIMENTAL_ONBOARDING_APP_ROUTE = '/app/experimental-onboarding'; diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/plugin.ts b/x-pack/plugins/observability_solution/observability_onboarding/public/plugin.ts index c83c26e3bf486..fd17f18085331 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/plugin.ts @@ -27,6 +27,7 @@ import { ObservabilityOnboardingLocatorDefinition } from './locators/onboarding_ import { ObservabilityOnboardingPluginLocators } from './locators'; import { ConfigSchema } from '.'; import { OBSERVABILITY_ONBOARDING_TELEMETRY_EVENT } from '../common/telemetry_events'; +import { EXPERIMENTAL_ONBOARDING_APP_ROUTE } from './common'; export type ObservabilityOnboardingPluginSetup = void; export type ObservabilityOnboardingPluginStart = void; @@ -115,7 +116,7 @@ export class ObservabilityOnboardingPlugin core.application.register({ id: `${PLUGIN_ID}_EXPERIMENTAL`, title: 'Observability Onboarding (Beta)', - appRoute: '/app/experimental-onboarding', + appRoute: EXPERIMENTAL_ONBOARDING_APP_ROUTE, order: 8500, euiIconType: 'logoObservability', category: DEFAULT_APP_CATEGORIES.observability,