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:
@@ -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);
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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';
|
||||
|
||||
+1
-9
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user