core-app-api: added AppRouter as replacement for app.getRouter()
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-app-api': minor
|
||||
---
|
||||
|
||||
Added a new `AppRouter` component that replaces the same component currently created through `app.getRouter()`.
|
||||
@@ -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[]);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { AppRouter } from './AppRouter';
|
||||
export { createSpecializedApp } from './createSpecializedApp';
|
||||
export { defaultConfigLoader } from './defaultConfigLoader';
|
||||
export * from './types';
|
||||
|
||||
@@ -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<{}>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user