From 9bc954b9f82b73b7be4f0c956b6660a2101307f5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 6 Mar 2021 23:53:58 +0100 Subject: [PATCH] core-api: implement SubRouteRef routing Signed-off-by: Patrik Oldsberg --- packages/core-api/src/app/App.test.tsx | 120 ++++++++++++++++-------- packages/core-api/src/app/types.ts | 3 +- packages/core-api/src/routing/hooks.tsx | 67 ++++++++++--- 3 files changed, 137 insertions(+), 53 deletions(-) diff --git a/packages/core-api/src/app/App.test.tsx b/packages/core-api/src/app/App.test.tsx index b9fdf05444..a263a452b9 100644 --- a/packages/core-api/src/app/App.test.tsx +++ b/packages/core-api/src/app/App.test.tsx @@ -23,7 +23,11 @@ import { createRoutableExtension } from '../extensions'; import { defaultSystemIcons } from '../icons'; import { createPlugin } from '../plugin'; import { useRouteRef } from '../routing/hooks'; -import { createExternalRouteRef, createRouteRef } from '../routing/RouteRef'; +import { + createExternalRouteRef, + createRouteRef, + createSubRouteRef, +} from '../routing/RouteRef'; import { generateBoundRoutes, PrivateAppImpl } from './App'; describe('generateBoundRoutes', () => { @@ -55,14 +59,37 @@ describe('Integration Test', () => { title: '', params: ['x'], }); - const err = createExternalRouteRef({ id: 'err' }); - const errParams = createExternalRouteRef({ id: 'errParams', params: ['x'] }); - const errOptional = createExternalRouteRef({ - id: 'errOptional', + const subRouteRef1 = createSubRouteRef({ + id: 'sub1', + path: '/sub1', + parent: plugin1RouteRef, + }); + const subRouteRef2 = createSubRouteRef({ + id: 'sub2', + path: '/sub2/:x', + parent: plugin1RouteRef, + }); + const subRouteRef3 = createSubRouteRef({ + id: 'sub3', + path: '/sub3', + parent: plugin2RouteRef, + }); + const subRouteRef4 = createSubRouteRef({ + id: 'sub4', + path: '/sub4/:y', + parent: plugin2RouteRef, + }); + const extRouteRef1 = createExternalRouteRef({ id: 'extRouteRef1' }); + const extRouteRef2 = createExternalRouteRef({ + id: 'extRouteRef2', + params: ['x'], + }); + const extRouteRef3 = createExternalRouteRef({ + id: 'extRouteRef3', optional: true, }); - const errParamsOptional = createExternalRouteRef({ - id: 'errParamsOptional', + const extRouteRef4 = createExternalRouteRef({ + id: 'extRouteRef4', optional: true, params: ['x'], }); @@ -70,10 +97,10 @@ describe('Integration Test', () => { const plugin1 = createPlugin({ id: 'blob', externalRoutes: { - err, - errParams, - errOptional, - errParamsOptional, + extRouteRef1, + extRouteRef2, + extRouteRef3, + extRouteRef4, }, }); @@ -92,19 +119,28 @@ describe('Integration Test', () => { createRoutableExtension({ component: () => Promise.resolve((_: PropsWithChildren<{ path?: string }>) => { - const errLink = useRouteRef(err); - const errParamsLink = useRouteRef(errParams); - const errOptionalLink = useRouteRef(errOptional); - const errParamsOptionalLink = useRouteRef(errParamsOptional); + const link1 = useRouteRef(plugin1RouteRef); + const link2 = useRouteRef(plugin2RouteRef); + const subLink1 = useRouteRef(subRouteRef1); + const subLink2 = useRouteRef(subRouteRef2); + const subLink3 = useRouteRef(subRouteRef3); + const subLink4 = useRouteRef(subRouteRef4); + const extLink1 = useRouteRef(extRouteRef1); + const extLink2 = useRouteRef(extRouteRef2); + const extLink3 = useRouteRef(extRouteRef3); + const extLink4 = useRouteRef(extRouteRef4); return (
- err: {errLink()} - errParams: {errParamsLink({ x: 'a' })} - errOptional: {errOptionalLink?.() ?? ''} - - errParamsOptional:{' '} - {errParamsOptionalLink?.({ x: 'b' }) ?? ''} - + link1: {link1()} + link2: {link2({ x: 'a' })} + subLink1: {subLink1()} + subLink2: {subLink2({ x: 'a' })} + subLink3: {subLink3({ x: 'b' })} + subLink4: {subLink4({ x: 'c', y: 'd' })} + extLink1: {extLink1()} + extLink2: {extLink2({ x: 'a' })} + extLink3: {extLink3?.() ?? ''} + extLink4: {extLink4?.({ x: 'b' }) ?? ''}
); }), @@ -136,10 +172,10 @@ describe('Integration Test', () => { components, bindRoutes: ({ bind }) => { bind(plugin1.externalRoutes, { - err: plugin1RouteRef, - errParams: plugin2RouteRef, - errOptional: plugin1RouteRef, - errParamsOptional: plugin2RouteRef, + extRouteRef1: plugin1RouteRef, + extRouteRef2: plugin2RouteRef, + extRouteRef3: subRouteRef1, + extRouteRef4: plugin2RouteRef, }); }, }); @@ -152,16 +188,22 @@ describe('Integration Test', () => { - + , ); - expect(screen.getByText('err: /')).toBeInTheDocument(); - expect(screen.getByText('errParams: /foo')).toBeInTheDocument(); - expect(screen.getByText('errOptional: /')).toBeInTheDocument(); - expect(screen.getByText('errParamsOptional: /foo')).toBeInTheDocument(); + expect(screen.getByText('link1: /')).toBeInTheDocument(); + expect(screen.getByText('link2: /foo/a')).toBeInTheDocument(); + expect(screen.getByText('subLink1: /sub1')).toBeInTheDocument(); + expect(screen.getByText('subLink2: /sub2/a')).toBeInTheDocument(); + expect(screen.getByText('subLink3: /foo/b/sub3')).toBeInTheDocument(); + expect(screen.getByText('subLink4: /foo/c/sub4/d')).toBeInTheDocument(); + expect(screen.getByText('extLink1: /')).toBeInTheDocument(); + expect(screen.getByText('extLink2: /foo/a')).toBeInTheDocument(); + expect(screen.getByText('extLink3: /sub1')).toBeInTheDocument(); + expect(screen.getByText('extLink4: /foo/b')).toBeInTheDocument(); }); it('runs happy paths without optional routes', async () => { @@ -181,8 +223,8 @@ describe('Integration Test', () => { components, bindRoutes: ({ bind }) => { bind(plugin1.externalRoutes, { - err: plugin1RouteRef, - errParams: plugin2RouteRef, + extRouteRef1: plugin1RouteRef, + extRouteRef2: plugin2RouteRef, }); }, }); @@ -201,10 +243,10 @@ describe('Integration Test', () => { , ); - expect(screen.getByText('err: /')).toBeInTheDocument(); - expect(screen.getByText('errParams: /foo')).toBeInTheDocument(); - expect(screen.getByText('errOptional: ')).toBeInTheDocument(); - expect(screen.getByText('errParamsOptional: ')).toBeInTheDocument(); + expect(screen.getByText('extLink1: /')).toBeInTheDocument(); + expect(screen.getByText('extLink2: /foo')).toBeInTheDocument(); + expect(screen.getByText('extLink3: ')).toBeInTheDocument(); + expect(screen.getByText('extLink4: ')).toBeInTheDocument(); }); it('should throw some error when the route has duplicate params', () => { @@ -224,8 +266,8 @@ describe('Integration Test', () => { components, bindRoutes: ({ bind }) => { bind(plugin1.externalRoutes, { - err: plugin1RouteRef, - errParams: plugin2RouteRef, + extRouteRef1: plugin1RouteRef, + extRouteRef2: plugin2RouteRef, }); }, }); diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index df4180c6e4..5bdf51837d 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -21,6 +21,7 @@ import { ExternalRouteRef, RouteRef } from '../routing'; import { AnyApiFactory } from '../apis'; import { AppTheme, ProfileInfo } from '../apis/definitions'; import { AppConfig } from '@backstage/config'; +import { SubRouteRef } from '../routing/types'; export type BootErrorPageProps = { step: 'load-config'; @@ -102,7 +103,7 @@ type TargetRouteMap = { infer Params, any > - ? RouteRef + ? RouteRef | SubRouteRef : never; }; diff --git a/packages/core-api/src/routing/hooks.tsx b/packages/core-api/src/routing/hooks.tsx index 4d419485f6..e37bb32f8b 100644 --- a/packages/core-api/src/routing/hooks.tsx +++ b/packages/core-api/src/routing/hooks.tsx @@ -15,14 +15,17 @@ */ import React, { createContext, ReactNode, useContext, useMemo } from 'react'; +import { generatePath, matchRoutes, useLocation } from 'react-router-dom'; import { AnyRouteRef, BackstageRouteObject, RouteRef, ExternalRouteRef, AnyParams, + SubRouteRef, + routeRefType, } from './types'; -import { generatePath, matchRoutes, useLocation } from 'react-router-dom'; +import { isRouteRef, isSubRouteRef, isExternalRouteRef } from './RouteRef'; // The extra TS magic here is to require a single params argument if the RouteRef // had at least one param defined, but require 0 arguments if there are no params defined. @@ -38,29 +41,64 @@ class RouteResolver { private readonly routePaths: Map, private readonly routeParents: Map, private readonly routeObjects: BackstageRouteObject[], - private readonly routeBindings: Map, + private readonly routeBindings: Map< + ExternalRouteRef, + RouteRef | SubRouteRef + >, ) {} resolve( - routeRefOrExternalRouteRef: RouteRef | ExternalRouteRef, + anyRouteRef: + | RouteRef + | SubRouteRef + | ExternalRouteRef, sourceLocation: ReturnType, ): RouteFunc | undefined { - const routeRef = - this.routeBindings.get(routeRefOrExternalRouteRef) ?? - (routeRefOrExternalRouteRef as RouteRef); + let resolvedRef: AnyRouteRef; + let subRoutePath = ''; + if (isRouteRef(anyRouteRef)) { + resolvedRef = anyRouteRef; + } else if (isSubRouteRef(anyRouteRef)) { + resolvedRef = anyRouteRef.parent; + subRoutePath = anyRouteRef.path; + } else if (isExternalRouteRef(anyRouteRef)) { + const resolvedRoute = this.routeBindings.get(anyRouteRef); + if (!resolvedRoute) { + return undefined; + } + if (isSubRouteRef(resolvedRoute)) { + subRoutePath = resolvedRoute.path; + resolvedRef = resolvedRoute.parent; + } else { + resolvedRef = resolvedRoute; + } + } else if (anyRouteRef[routeRefType]) { + throw new Error( + `Unknown or invalid route ref type, ${anyRouteRef[routeRefType]}`, + ); + } else { + throw new Error( + `Unknown object passed to useRouteRef, got ${anyRouteRef}`, + ); + } const match = matchRoutes(this.routeObjects, sourceLocation) ?? []; // If our route isn't bound to a path we fail the resolution and let the caller decide the failure mode - const lastPath = this.routePaths.get(routeRef); - if (!lastPath) { + const resolvedPath = this.routePaths.get(resolvedRef); + if (!resolvedPath) { return undefined; } + // SubRouteRefs join the path from the parent route with its own path + const lastPath = + resolvedPath + + (resolvedPath.endsWith('/') ? subRoutePath.slice(1) : subRoutePath); + const targetRefStack = Array(); let matchIndex = -1; for ( - let currentRouteRef: AnyRouteRef | undefined = routeRef; + let currentRouteRef: AnyRouteRef | undefined = resolvedRef; currentRouteRef; currentRouteRef = this.routeParents.get(currentRouteRef) ) { @@ -98,7 +136,7 @@ class RouteResolver { } if (path.includes(':')) { throw new Error( - `Cannot route to ${routeRef} with parent ${ref} as it has parameters`, + `Cannot route to ${resolvedRef} with parent ${ref} as it has parameters`, ); } return path; @@ -119,10 +157,13 @@ export function useRouteRef( routeRef: ExternalRouteRef, ): Optional extends true ? RouteFunc | undefined : RouteFunc; export function useRouteRef( - routeRef: RouteRef, + routeRef: RouteRef | SubRouteRef, ): RouteFunc; export function useRouteRef( - routeRef: RouteRef | ExternalRouteRef, + routeRef: + | RouteRef + | SubRouteRef + | ExternalRouteRef, ): RouteFunc | undefined { const sourceLocation = useLocation(); const resolver = useContext(RoutingContext); @@ -147,7 +188,7 @@ type ProviderProps = { routePaths: Map; routeParents: Map; routeObjects: BackstageRouteObject[]; - routeBindings: Map; + routeBindings: Map; children: ReactNode; };