core-app-api: added AppRouter as replacement for app.getRouter()

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2022-12-07 22:51:37 +01:00
parent c5c099450f
commit e0d9c9559a
7 changed files with 233 additions and 142 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-app-api': minor
---
Added a new `AppRouter` component that replaces the same component currently created through `app.getRouter()`.
+3
View File
@@ -227,6 +227,9 @@ export type AppRouteBinder = <
>,
) => void;
// @public
export function AppRouter({ children }: { children?: ReactNode }): JSX.Element;
// @public
export class AppThemeSelector implements AppThemeApi {
constructor(themes: AppTheme[]);
+6 -142
View File
@@ -17,15 +17,10 @@
import { AppConfig, Config } from '@backstage/config';
import React, {
ComponentType,
createContext,
PropsWithChildren,
ReactElement,
useContext,
useMemo,
useRef,
useState,
} from 'react';
import { Route, Routes } from 'react-router-dom';
import useAsync from 'react-use/lib/useAsync';
import {
ApiProvider,
@@ -34,7 +29,6 @@ import {
LocalStorageFeatureFlags,
} from '../apis';
import {
useApi,
AnyApiFactory,
ApiHolder,
IconComponent,
@@ -44,7 +38,6 @@ import {
AppThemeApi,
ConfigApi,
featureFlagsApiRef,
IdentityApi,
identityApiRef,
BackstagePlugin,
} from '@backstage/core-plugin-api';
@@ -61,7 +54,6 @@ import {
routingV2Collector,
} from '../routing/collectors';
import { RoutingProvider } from '../routing/RoutingProvider';
import { RouteTracker } from '../routing/RouteTracker';
import {
validateRouteParameters,
validateRouteBindings,
@@ -74,14 +66,14 @@ import {
AppContext,
AppOptions,
BackstageApp,
SignInPageProps,
} from './types';
import { AppThemeProvider } from './AppThemeProvider';
import { defaultConfigLoader } from './defaultConfigLoader';
import { ApiRegistry } from '../apis/system/ApiRegistry';
import { resolveRouteBindings } from './resolveRouteBindings';
import { BackstageRouteObject } from '../routing/types';
import { isReactRouterBeta } from './isReactRouterBeta';
import { InternalAppContext } from './InternalAppContext';
import { AppRouter, getBasePath } from './AppRouter';
type CompatiblePlugin =
| BackstagePlugin
@@ -89,39 +81,6 @@ type CompatiblePlugin =
output(): Array<{ type: 'feature-flag'; name: string }>;
});
const InternalAppContext = createContext<{
routeObjects: BackstageRouteObject[];
}>({ routeObjects: [] });
/**
* Get the app base path from the configured app baseUrl.
*
* The returned path does not have a trailing slash.
*/
function getBasePath(configApi: Config) {
if (!isReactRouterBeta()) {
// When using rr v6 stable the base path is handled through the
// basename prop on the router component instead.
return '';
}
return readBasePath(configApi);
}
/**
* Read the configured base path.
*
* The returned path does not have a trailing slash.
*/
function readBasePath(configApi: ConfigApi) {
let { pathname } = new URL(
configApi.getOptionalString('app.baseUrl') ?? '/',
'http://sample.dev', // baseUrl can be specified as just a path
);
pathname = pathname.replace(/\/*$/, '');
return pathname;
}
function useConfigLoader(
configLoader: AppConfigLoader | undefined,
components: AppComponents,
@@ -413,7 +372,10 @@ export class AppManager implements BackstageApp {
basePath={getBasePath(loadedConfig.api)}
>
<InternalAppContext.Provider
value={{ routeObjects: routing.objects }}
value={{
routeObjects: routing.objects,
appIdentityProxy: this.appIdentityProxy,
}}
>
{children}
</InternalAppContext.Provider>
@@ -427,104 +389,6 @@ export class AppManager implements BackstageApp {
}
getRouter(): ComponentType<{}> {
const { Router: RouterComponent, SignInPage: SignInPageComponent } =
this.components;
// This wraps the sign-in page and waits for sign-in to be completed before rendering the app
const SignInPageWrapper = ({
component: Component,
children,
}: {
component: ComponentType<SignInPageProps>;
children: ReactElement;
}) => {
const [identityApi, setIdentityApi] = useState<IdentityApi>();
const configApi = useApi(configApiRef);
const basePath = getBasePath(configApi);
if (!identityApi) {
return <Component onSignInSuccess={setIdentityApi} />;
}
this.appIdentityProxy.setTarget(identityApi, {
signOutTargetUrl: basePath || '/',
});
return children;
};
const AppRouter = ({ children }: PropsWithChildren<{}>) => {
const configApi = useApi(configApiRef);
const basePath = readBasePath(configApi);
const mountPath = `${basePath}/*`;
const { routeObjects } = useContext(InternalAppContext);
// If the app hasn't configured a sign-in page, we just continue as guest.
if (!SignInPageComponent) {
this.appIdentityProxy.setTarget(
{
getUserId: () => 'guest',
getIdToken: async () => undefined,
getProfile: () => ({
email: 'guest@example.com',
displayName: 'Guest',
}),
getProfileInfo: async () => ({
email: 'guest@example.com',
displayName: 'Guest',
}),
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest'],
}),
getCredentials: async () => ({}),
signOut: async () => {},
},
{ signOutTargetUrl: basePath || '/' },
);
if (isReactRouterBeta()) {
return (
<RouterComponent>
<RouteTracker routeObjects={routeObjects} />
<Routes>
<Route path={mountPath} element={<>{children}</>} />
</Routes>
</RouterComponent>
);
}
return (
<RouterComponent basename={basePath}>
<RouteTracker routeObjects={routeObjects} />
{children}
</RouterComponent>
);
}
if (isReactRouterBeta()) {
return (
<RouterComponent>
<RouteTracker routeObjects={routeObjects} />
<SignInPageWrapper component={SignInPageComponent}>
<Routes>
<Route path={mountPath} element={<>{children}</>} />
</Routes>
</SignInPageWrapper>
</RouterComponent>
);
}
return (
<RouterComponent basename={basePath}>
<RouteTracker routeObjects={routeObjects} />
<SignInPageWrapper component={SignInPageComponent}>
<>{children}</>
</SignInPageWrapper>
</RouterComponent>
);
};
return AppRouter;
}
+189
View File
@@ -0,0 +1,189 @@
/*
* Copyright 2022 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 React, { useContext, ReactNode, ComponentType, useState } from 'react';
import {
ConfigApi,
configApiRef,
IdentityApi,
SignInPageProps,
useApi,
useApp,
} from '@backstage/core-plugin-api';
import { InternalAppContext } from './InternalAppContext';
import { isReactRouterBeta } from './isReactRouterBeta';
import { RouteTracker } from '../routing/RouteTracker';
import { Route, Routes } from 'react-router-dom';
import { AppIdentityProxy } from '../apis/implementations/IdentityApi/AppIdentityProxy';
/**
* Get the app base path from the configured app baseUrl.
*
* The returned path does not have a trailing slash.
*/
export function getBasePath(configApi: ConfigApi) {
if (!isReactRouterBeta()) {
// When using rr v6 stable the base path is handled through the
// basename prop on the router component instead.
return '';
}
return readBasePath(configApi);
}
/**
* Read the configured base path.
*
* The returned path does not have a trailing slash.
*/
function readBasePath(configApi: ConfigApi) {
let { pathname } = new URL(
configApi.getOptionalString('app.baseUrl') ?? '/',
'http://sample.dev', // baseUrl can be specified as just a path
);
pathname = pathname.replace(/\/*$/, '');
return pathname;
}
// This wraps the sign-in page and waits for sign-in to be completed before rendering the app
function SignInPageWrapper({
component: Component,
appIdentityProxy,
children,
}: {
component: ComponentType<SignInPageProps>;
appIdentityProxy: AppIdentityProxy;
children: ReactNode;
}) {
const [identityApi, setIdentityApi] = useState<IdentityApi>();
const configApi = useApi(configApiRef);
const basePath = getBasePath(configApi);
if (!identityApi) {
return <Component onSignInSuccess={setIdentityApi} />;
}
appIdentityProxy.setTarget(identityApi, {
signOutTargetUrl: basePath || '/',
});
return <>{children}</>;
}
/**
* Props for the {@link AppRouter} component.
* @public
*/
export interface AppRouterProps {
children?: ReactNode;
}
/**
* App router and sign-in page wrapper.
*
* @public
* @remarks
*
* The AppRouter provides the routing context and renders the sign-in page.
* Until the user has successfully signed in, this component will render
* the sign-in page. Once the user has signed-in, it will instead render
* the app, while providing routing and route tracking for the app.
*
*/
export function AppRouter({ children }: { children?: ReactNode }) {
const { Router: RouterComponent, SignInPage: SignInPageComponent } =
useApp().getComponents();
const configApi = useApi(configApiRef);
const basePath = readBasePath(configApi);
const mountPath = `${basePath}/*`;
const internalAppContext = useContext(InternalAppContext);
if (!internalAppContext) {
throw new Error('AppRouter must be rendered within the AppProvider');
}
const { routeObjects, appIdentityProxy } = internalAppContext;
// If the app hasn't configured a sign-in page, we just continue as guest.
if (!SignInPageComponent) {
appIdentityProxy.setTarget(
{
getUserId: () => 'guest',
getIdToken: async () => undefined,
getProfile: () => ({
email: 'guest@example.com',
displayName: 'Guest',
}),
getProfileInfo: async () => ({
email: 'guest@example.com',
displayName: 'Guest',
}),
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest'],
}),
getCredentials: async () => ({}),
signOut: async () => {},
},
{ signOutTargetUrl: basePath || '/' },
);
if (isReactRouterBeta()) {
return (
<RouterComponent>
<RouteTracker routeObjects={routeObjects} />
<Routes>
<Route path={mountPath} element={<>{children}</>} />
</Routes>
</RouterComponent>
);
}
return (
<RouterComponent basename={basePath}>
<RouteTracker routeObjects={routeObjects} />
{children}
</RouterComponent>
);
}
if (isReactRouterBeta()) {
return (
<RouterComponent>
<RouteTracker routeObjects={routeObjects} />
<SignInPageWrapper
component={SignInPageComponent}
appIdentityProxy={appIdentityProxy}
>
<Routes>
<Route path={mountPath} element={<>{children}</>} />
</Routes>
</SignInPageWrapper>
</RouterComponent>
);
}
return (
<RouterComponent basename={basePath}>
<RouteTracker routeObjects={routeObjects} />
<SignInPageWrapper
component={SignInPageComponent}
appIdentityProxy={appIdentityProxy}
>
{children}
</SignInPageWrapper>
</RouterComponent>
);
}
@@ -0,0 +1,27 @@
/*
* Copyright 2022 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 { createContext } from 'react';
import { AppIdentityProxy } from '../apis/implementations/IdentityApi/AppIdentityProxy';
import { BackstageRouteObject } from '../routing/types';
export const InternalAppContext = createContext<
| undefined
| {
routeObjects: BackstageRouteObject[];
appIdentityProxy: AppIdentityProxy;
}
>(undefined);
+1
View File
@@ -14,6 +14,7 @@
* limitations under the License.
*/
export { AppRouter } from './AppRouter';
export { createSpecializedApp } from './createSpecializedApp';
export { defaultConfigLoader } from './defaultConfigLoader';
export * from './types';
+2
View File
@@ -307,6 +307,8 @@ export type BackstageApp = {
/**
* Router component that should wrap the App Routes create with getRoutes()
* and any other components that should only be available while signed in.
*
* @deprecated Import and use the {@link AppRouter} component from `@backstage/core-app-api` instead
*/
getRouter(): ComponentType<{}>;
};