diff --git a/packages/shared-ux/chrome/navigation/__jest__/build_nav_tree.test.tsx b/packages/shared-ux/chrome/navigation/__jest__/build_nav_tree.test.tsx index 4947bcabac5520..df8246df69d5ea 100644 --- a/packages/shared-ux/chrome/navigation/__jest__/build_nav_tree.test.tsx +++ b/packages/shared-ux/chrome/navigation/__jest__/build_nav_tree.test.tsx @@ -22,6 +22,9 @@ import { type TestType, type ProjectNavigationChangeListener, } from './utils'; +import { getServicesMock } from '../mocks/src/jest'; + +const { cloudLinks: mockCloudLinks } = getServicesMock(); describe('builds navigation tree', () => { test('render reference UI and build the navigation tree', async () => { @@ -675,21 +678,41 @@ describe('builds navigation tree', () => { }); test('should render the cloud links', async () => { + const stripLastChar = (str: string = '') => str.substring(0, str.length - 1); + const runTests = async (type: TestType, { findByTestId }: RenderResult) => { try { expect(await findByTestId(/nav-item-group1.cloudLink1/)).toBeVisible(); expect(await findByTestId(/nav-item-group1.cloudLink2/)).toBeVisible(); expect(await findByTestId(/nav-item-group1.cloudLink3/)).toBeVisible(); - expect((await findByTestId(/nav-item-group1.cloudLink1/)).textContent).toBe( - 'Mock Users & RolesExternal link' - ); - expect((await findByTestId(/nav-item-group1.cloudLink2/)).textContent).toBe( - 'Mock PerformanceExternal link' - ); - expect((await findByTestId(/nav-item-group1.cloudLink3/)).textContent).toBe( - 'Mock Billing & SubscriptionsExternal link' - ); + { + const userAndRolesLink = await findByTestId(/nav-item-group1.cloudLink1/); + expect(userAndRolesLink.textContent).toBe('Mock Users & RolesExternal link'); + const href = userAndRolesLink.getAttribute('href'); + expect(href).toBe(stripLastChar(mockCloudLinks.userAndRoles?.href)); + } + + { + const performanceLink = await findByTestId(/nav-item-group1.cloudLink2/); + expect(performanceLink.textContent).toBe('Mock PerformanceExternal link'); + const href = performanceLink.getAttribute('href'); + expect(href).toBe(stripLastChar(mockCloudLinks.performance?.href)); + } + + { + const billingLink = await findByTestId(/nav-item-group1.cloudLink3/); + expect(billingLink.textContent).toBe('Mock Billing & SubscriptionsExternal link'); + const href = billingLink.getAttribute('href'); + expect(href).toBe(stripLastChar(mockCloudLinks.billingAndSub?.href)); + } + + { + const deploymentLink = await findByTestId(/nav-item-group1.cloudLink4/); + expect(deploymentLink.textContent).toBe('Mock DeploymentExternal link'); + const href = deploymentLink.getAttribute('href'); + expect(href).toBe(stripLastChar(mockCloudLinks.deployment?.href)); + } } catch (e) { errorHandler(type)(e); } @@ -706,6 +729,7 @@ describe('builds navigation tree', () => { { id: 'cloudLink1', cloudLink: 'userAndRoles' }, { id: 'cloudLink2', cloudLink: 'performance' }, { id: 'cloudLink3', cloudLink: 'billingAndSub' }, + { id: 'cloudLink4', cloudLink: 'deployment' }, ], }, ]; @@ -727,6 +751,7 @@ describe('builds navigation tree', () => { + ), diff --git a/packages/shared-ux/chrome/navigation/mocks/src/jest.ts b/packages/shared-ux/chrome/navigation/mocks/src/jest.ts index e1761de6be6fdf..9ae6c2b2a6952e 100644 --- a/packages/shared-ux/chrome/navigation/mocks/src/jest.ts +++ b/packages/shared-ux/chrome/navigation/mocks/src/jest.ts @@ -33,15 +33,19 @@ export const getServicesMock = ({ cloudLinks: { billingAndSub: { title: 'Mock Billing & Subscriptions', - href: 'https://cloud.elastic.co/account/billing', + href: 'https://cloud.elastic.co/account/billing/', }, performance: { title: 'Mock Performance', - href: 'https://cloud.elastic.co/deployments/123456789/performance', + href: 'https://cloud.elastic.co/deployments/123456789/performance/', }, userAndRoles: { title: 'Mock Users & Roles', - href: 'https://cloud.elastic.co/deployments/123456789/security/users', + href: 'https://cloud.elastic.co/deployments/123456789/security/users/', + }, + deployment: { + title: 'Mock Deployment', + href: 'https://cloud.elastic.co/deployments/123456789/', }, }, }; diff --git a/packages/shared-ux/chrome/navigation/src/cloud_links.tsx b/packages/shared-ux/chrome/navigation/src/cloud_links.tsx index fd1909551888bf..ce9d9e5990aaba 100644 --- a/packages/shared-ux/chrome/navigation/src/cloud_links.tsx +++ b/packages/shared-ux/chrome/navigation/src/cloud_links.tsx @@ -9,11 +9,13 @@ import { i18n } from '@kbn/i18n'; import type { CloudLinkId } from '@kbn/core-chrome-browser'; import type { CloudStart } from '@kbn/cloud-plugin/public'; +export interface CloudLink { + title: string; + href: string; +} + export type CloudLinks = { - [id in CloudLinkId]?: { - title: string; - href: string; - }; + [id in CloudLinkId]?: CloudLink; }; export const getCloudLinks = (cloud: CloudStart): CloudLinks => { diff --git a/packages/shared-ux/chrome/navigation/src/services.tsx b/packages/shared-ux/chrome/navigation/src/services.tsx index fd3f774bd72fba..f77a288160a3dc 100644 --- a/packages/shared-ux/chrome/navigation/src/services.tsx +++ b/packages/shared-ux/chrome/navigation/src/services.tsx @@ -9,15 +9,45 @@ import React, { FC, useContext, useMemo } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { NavigationKibanaDependencies, NavigationServices } from '../types'; -import { CloudLinks, getCloudLinks } from './cloud_links'; +import { CloudLink, CloudLinks, getCloudLinks } from './cloud_links'; const Context = React.createContext(null); +const stripTrailingForwardSlash = (str: string) => { + return str[str.length - 1] === '/' ? str.substring(0, str.length - 1) : str; +}; + +const parseCloudURLs = (cloudLinks: CloudLinks): CloudLinks => { + const { userAndRoles, billingAndSub, deployment, performance } = cloudLinks; + + // We remove potential trailing forward slash ("/") at the end of the URL + // because it breaks future navigation in Cloud console once we navigate there. + const parseLink = (link?: CloudLink): CloudLink | undefined => { + if (!link) return undefined; + return { ...link, href: stripTrailingForwardSlash(link.href) }; + }; + + return { + ...cloudLinks, + userAndRoles: parseLink(userAndRoles), + billingAndSub: parseLink(billingAndSub), + deployment: parseLink(deployment), + performance: parseLink(performance), + }; +}; + /** * A Context Provider that provides services to the component and its dependencies. */ export const NavigationProvider: FC = ({ children, ...services }) => { - return {children}; + const servicesParsed = useMemo(() => { + return { + ...services, + cloudLinks: parseCloudURLs(services.cloudLinks), + }; + }, [services]); + + return {children}; }; /** @@ -32,7 +62,10 @@ export const NavigationKibanaProvider: FC = ({ const { basePath } = http; const { navigateToUrl } = core.application; - const cloudLinks: CloudLinks = useMemo(() => (cloud ? getCloudLinks(cloud) : {}), [cloud]); + const cloudLinks: CloudLinks = useMemo( + () => (cloud ? parseCloudURLs(getCloudLinks(cloud)) : {}), + [cloud] + ); const isSideNavCollapsed = useObservable(chrome.getIsSideNavCollapsed$(), true); const value: NavigationServices = {