Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 });
}}
>
{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 @@ -320,12 +320,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 @@ -338,7 +338,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 @@ -395,7 +395,7 @@ export function Detail() {
</EuiFlexItem>
</EuiFlexGroup>
),
[integrationInfo, isLoading, packageInfo, href, queryParams]
[integrationInfo, isLoading, packageInfo, fromIntegrationsPath, queryParams]
);

const handleAddIntegrationPolicyClick = useCallback<ReactEventHandler>(
Expand All @@ -421,20 +421,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 @@ -452,8 +452,8 @@ export function Detail() {
isExperimentalAddIntegrationPageEnabled,
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