Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<I18nProvider>
<BackLink queryParams={queryParams} href="/app/integrations" />
</I18nProvider>
);
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(
<I18nProvider>
<BackLink queryParams={queryParams} href="/app/integrations" />
</I18nProvider>
);
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(
<I18nProvider>
<BackLink queryParams={queryParams} href="/app/integrations" />
<BackLink queryParams={queryParams} integrationsPath="/browse" />
</I18nProvider>
);
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(
<I18nProvider>
<BackLink queryParams={queryParams} href="/app/integrations" />
<BackLink queryParams={queryParams} integrationsPath="/browse" />
</I18nProvider>
);
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,
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<EuiButtonEmpty iconType="arrowLeft" size="xs" flush="left" href={href}>
{message}
</EuiButtonEmpty>
<>
<EuiButtonEmpty
iconType="arrowLeft"
size="xs"
flush="left"
onClick={() => {
navigateToApp(appId, { path });
Comment thread
kgeller marked this conversation as resolved.
}}
>
{message}
</EuiButtonEmpty>
</>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand All @@ -336,7 +336,7 @@ export function Detail() {
<EuiFlexItem>
{/* Allows button to break out of full width */}
<div>
<BackLink queryParams={queryParams} href={href} />
<BackLink queryParams={queryParams} integrationsPath={fromIntegrationsPath} />
</div>
</EuiFlexItem>
<EuiFlexItem>
Expand Down Expand Up @@ -393,7 +393,7 @@ export function Detail() {
</EuiFlexItem>
</EuiFlexGroup>
),
[integrationInfo, isLoading, packageInfo, href, queryParams]
[integrationInfo, isLoading, packageInfo, fromIntegrationsPath, queryParams]
);

const handleAddIntegrationPolicyClick = useCallback<ReactEventHandler>(
Expand All @@ -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 }],
},
},
]
Expand All @@ -448,8 +448,8 @@ export function Detail() {
isCloud,
isFirstTimeAgentUser,
isGuidedOnboardingActive,
onboardingAppId,
onboardingLink,
returnAppId,
returnPath,
packageInfo,
pathname,
pkgkey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
Loading
Loading