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;
};