frontend-app-api: initial routing system implementation

Co-authored-by: Fredrik Adelöw <freben@gmail.com>
Co-authored-by: Philipp Hugenroth <philipph@spotify.com>
Co-authored-by: Johan Haals <johan.haals@gmail.com>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-09-01 15:05:18 +02:00
parent 91a534142d
commit 4d410e0985
10 changed files with 102 additions and 15 deletions
@@ -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 }) => <Router />),
});
@@ -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);
+1
View File
@@ -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:^",
+36 -2
View File
@@ -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 (
<>
<RoutingProvider routePaths={routePaths}>
{rootComponents.map((Component, i) => (
<Component key={i} />
))}
</>
</RoutingProvider>
);
},
};
}
export function extractRouteInfoFromInstanceTree(
roots: ExtensionInstance[],
): Map<RouteRef, string> {
const results = new Map<RouteRef, string>();
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;
}
@@ -28,6 +28,7 @@ export const RouteExtension = createExtension({
routes: {
extensionData: {
path: coreExtensionData.routePath,
ref: coreExtensionData.routeRef,
component: coreExtensionData.reactComponent,
},
},
@@ -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<RoutingContextType>({
resolve: () => () => '',
});
export class RouteResolver {
constructor(private readonly routePaths: Map<RouteRef, string>) {}
resolve(anyRouteRef: RouteRef<{}>): (() => string) | undefined {
const basePath = this.routePaths.get(anyRouteRef);
if (!basePath) {
return undefined;
}
return () => basePath;
}
}
export function RoutingProvider(props: {
routePaths: Map<RouteRef, string>;
children?: ReactNode;
}) {
return (
<RoutingContext.Provider value={new RouteResolver(props.routePaths)}>
{props.children}
</RoutingContext.Provider>
);
}
@@ -37,3 +37,4 @@ export {
type ExtensionDataValue,
} from './types';
export * from './wiring';
export * from './routing';
@@ -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<RoutingContextType>({
resolve: () => () => '',
});
export { useRouteRef } from './useRouteRef';
@@ -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<any>): () => string | undefined {
export function useRouteRef(routeRef: RouteRef<any>): () => string {
const { pathname } = useLocation();
const resolver = useContext(RoutingContext);
@@ -28,5 +29,9 @@ export function useRouteRef(routeRef: RouteRef<any>): () => string | undefined {
[resolver, routeRef, pathname],
);
if (!routeFunc) {
throw new Error(`Failed to resolve routeRef ${routeRef}`);
}
return routeFunc;
}