diff --git a/packages/app-next/src/examples/graphiqlPlugin.tsx b/packages/app-next/src/examples/graphiqlPlugin.tsx index e80e137ae5..4e3768dafa 100644 --- a/packages/app-next/src/examples/graphiqlPlugin.tsx +++ b/packages/app-next/src/examples/graphiqlPlugin.tsx @@ -23,7 +23,6 @@ import React from 'react'; export const GraphiqlPage = createPageExtension({ id: 'graphiql.page', defaultPath: '/graphiql', - disabled: true, component: () => import('@backstage/plugin-graphiql').then(({ Router }) => ), }); diff --git a/packages/app-next/src/examples/pagesPlugin.tsx b/packages/app-next/src/examples/pagesPlugin.tsx index 4551f0c8f7..4bf41b772a 100644 --- a/packages/app-next/src/examples/pagesPlugin.tsx +++ b/packages/app-next/src/examples/pagesPlugin.tsx @@ -21,7 +21,7 @@ import { createPlugin, useRouteRef, } from '@backstage/frontend-plugin-api'; -import { createRouteRef, createSubRouteRef } from '@backstage/core-plugin-api'; +import { createRouteRef } from '@backstage/core-plugin-api'; import { Route, Routes } from 'react-router-dom'; const indexRouteRef = createRouteRef({ id: 'index' }); @@ -35,7 +35,7 @@ const page1RouteRef = createRouteRef({ id: 'page1' }); const IndexPage = createPageExtension({ id: 'index', defaultPath: '/', - // indexRouteRef + routeRef: indexRouteRef, component: async () => { const Component = () => { const page1Link = useRouteRef(page1RouteRef); diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index b1f0097a9d..76a7830707 100644 --- a/packages/frontend-app-api/package.json +++ b/packages/frontend-app-api/package.json @@ -33,6 +33,7 @@ ], "dependencies": { "@backstage/config": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-graphiql": "workspace:^", "@backstage/types": "workspace:^", diff --git a/packages/frontend-app-api/src/createApp.tsx b/packages/frontend-app-api/src/createApp.tsx index 192e2485c6..a52b5c95d7 100644 --- a/packages/frontend-app-api/src/createApp.tsx +++ b/packages/frontend-app-api/src/createApp.tsx @@ -30,6 +30,8 @@ import { mergeExtensionParameters, readAppExtensionParameters, } from './wiring/parameters'; +import { RoutingProvider } from './routing/RoutingContext'; +import { RouteRef } from '@backstage/core-plugin-api'; /** @public */ export function createApp(options: { plugins: BackstagePlugin[] }): { @@ -109,6 +111,8 @@ export function createApp(options: { plugins: BackstagePlugin[] }): { createInstance(instanceParams), ); + const routePaths = extractRouteInfoFromInstanceTree(rootInstances); + return { createRoot() { const rootComponents = rootInstances.map( @@ -118,12 +122,42 @@ export function createApp(options: { plugins: BackstagePlugin[] }): { ) as typeof coreExtensionData.reactComponent.T, ); return ( - <> + {rootComponents.map((Component, i) => ( ))} - + ); }, }; } + +export function extractRouteInfoFromInstanceTree( + roots: ExtensionInstance[], +): Map { + const results = new Map(); + + function visit(current: ExtensionInstance, basePath: string) { + const routePath = current.data.get(coreExtensionData.routePath.id) ?? ''; + const routeRef = current.data.get( + coreExtensionData.routeRef.id, + ) as RouteRef; + + // TODO: join paths in a more robust way + const fullPath = basePath + routePath; + if (routeRef) { + results.set(routeRef, fullPath); + } + + for (const children of current.attachments.values()) { + for (const child of children) { + visit(child, fullPath); + } + } + } + + for (const root of roots) { + visit(root, ''); + } + return results; +} diff --git a/packages/frontend-app-api/src/extensions/RouteExtension.tsx b/packages/frontend-app-api/src/extensions/RouteExtension.tsx index dfa5874d70..ffbe802413 100644 --- a/packages/frontend-app-api/src/extensions/RouteExtension.tsx +++ b/packages/frontend-app-api/src/extensions/RouteExtension.tsx @@ -28,6 +28,7 @@ export const RouteExtension = createExtension({ routes: { extensionData: { path: coreExtensionData.routePath, + ref: coreExtensionData.routeRef, component: coreExtensionData.reactComponent, }, }, diff --git a/packages/frontend-app-api/src/routing/RoutingContext.tsx b/packages/frontend-app-api/src/routing/RoutingContext.tsx new file mode 100644 index 0000000000..237c8aa5df --- /dev/null +++ b/packages/frontend-app-api/src/routing/RoutingContext.tsx @@ -0,0 +1,52 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RouteRef } from '@backstage/core-plugin-api'; +import React, { createContext, ReactNode } from 'react'; + +export interface RoutingContextType { + resolve( + routeRef: RouteRef, + options: { pathname: string }, + ): (() => string) | undefined; +} + +export const RoutingContext = createContext({ + resolve: () => () => '', +}); + +export class RouteResolver { + constructor(private readonly routePaths: Map) {} + + resolve(anyRouteRef: RouteRef<{}>): (() => string) | undefined { + const basePath = this.routePaths.get(anyRouteRef); + if (!basePath) { + return undefined; + } + return () => basePath; + } +} + +export function RoutingProvider(props: { + routePaths: Map; + children?: ReactNode; +}) { + return ( + + {props.children} + + ); +} diff --git a/packages/frontend-plugin-api/src/index.ts b/packages/frontend-plugin-api/src/index.ts index 8c87986a14..16f226c483 100644 --- a/packages/frontend-plugin-api/src/index.ts +++ b/packages/frontend-plugin-api/src/index.ts @@ -37,3 +37,4 @@ export { type ExtensionDataValue, } from './types'; export * from './wiring'; +export * from './routing'; diff --git a/packages/frontend-app-api/src/routing/RoutingContext.ts b/packages/frontend-plugin-api/src/routing/index.ts similarity index 66% rename from packages/frontend-app-api/src/routing/RoutingContext.ts rename to packages/frontend-plugin-api/src/routing/index.ts index 968cb82d5f..4506538fa0 100644 --- a/packages/frontend-app-api/src/routing/RoutingContext.ts +++ b/packages/frontend-plugin-api/src/routing/index.ts @@ -13,13 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { RouteRef } from '@backstage/core-plugin-api'; -import { createContext } from 'react'; -export interface RoutingContextType { - resolve(routeRef: RouteRef, options: { pathname: string }): () => string; -} - -export const RoutingContext = createContext({ - resolve: () => () => '', -}); +export { useRouteRef } from './useRouteRef'; diff --git a/packages/frontend-plugin-api/src/routing/useRouteRef.ts b/packages/frontend-plugin-api/src/routing/useRouteRef.ts index f828240966..fa50522bca 100644 --- a/packages/frontend-plugin-api/src/routing/useRouteRef.ts +++ b/packages/frontend-plugin-api/src/routing/useRouteRef.ts @@ -15,11 +15,12 @@ */ import { RouteRef } from '@backstage/core-plugin-api'; +// eslint-disable-next-line @backstage/no-forbidden-package-imports import { RoutingContext } from '@backstage/frontend-app-api/src/routing/RoutingContext'; import { useContext, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; -export function useRouteRef(routeRef: RouteRef): () => string | undefined { +export function useRouteRef(routeRef: RouteRef): () => string { const { pathname } = useLocation(); const resolver = useContext(RoutingContext); @@ -28,5 +29,9 @@ export function useRouteRef(routeRef: RouteRef): () => string | undefined { [resolver, routeRef, pathname], ); + if (!routeFunc) { + throw new Error(`Failed to resolve routeRef ${routeRef}`); + } + return routeFunc; } diff --git a/yarn.lock b/yarn.lock index 2e94de61cb..472549f1f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4311,6 +4311,7 @@ __metadata: dependencies: "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-graphiql": "workspace:^" "@backstage/types": "workspace:^" @@ -4337,6 +4338,7 @@ __metadata: zod-to-json-schema: ^3.21.4 peerDependencies: react: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 languageName: unknown linkType: soft