diff --git a/.changeset/fuzzy-rivers-search.md b/.changeset/fuzzy-rivers-search.md
new file mode 100644
index 0000000000..06e2dda6d7
--- /dev/null
+++ b/.changeset/fuzzy-rivers-search.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-app-api': minor
+---
+
+Added a new `AppRouter` component that replaces the same component currently created through `app.getRouter()`.
diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md
index 70312a54a5..137d6d72ca 100644
--- a/packages/core-app-api/api-report.md
+++ b/packages/core-app-api/api-report.md
@@ -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[]);
diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx
index f4fa2d8442..a5c981a54e 100644
--- a/packages/core-app-api/src/app/AppManager.tsx
+++ b/packages/core-app-api/src/app/AppManager.tsx
@@ -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)}
>
{children}
@@ -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;
- children: ReactElement;
- }) => {
- const [identityApi, setIdentityApi] = useState();
- const configApi = useApi(configApiRef);
- const basePath = getBasePath(configApi);
-
- if (!identityApi) {
- return ;
- }
-
- 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 (
-
-
-
- {children}>} />
-
-
- );
- }
-
- return (
-
-
- {children}
-
- );
- }
-
- if (isReactRouterBeta()) {
- return (
-
-
-
-
- {children}>} />
-
-
-
- );
- }
-
- return (
-
-
-
- <>{children}>
-
-
- );
- };
-
return AppRouter;
}
diff --git a/packages/core-app-api/src/app/AppRouter.tsx b/packages/core-app-api/src/app/AppRouter.tsx
new file mode 100644
index 0000000000..c95bdaa7c5
--- /dev/null
+++ b/packages/core-app-api/src/app/AppRouter.tsx
@@ -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;
+ appIdentityProxy: AppIdentityProxy;
+ children: ReactNode;
+}) {
+ const [identityApi, setIdentityApi] = useState();
+ const configApi = useApi(configApiRef);
+ const basePath = getBasePath(configApi);
+
+ if (!identityApi) {
+ return ;
+ }
+
+ 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 (
+
+
+
+ {children}>} />
+
+
+ );
+ }
+
+ return (
+
+
+ {children}
+
+ );
+ }
+
+ if (isReactRouterBeta()) {
+ return (
+
+
+
+
+ {children}>} />
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/packages/core-app-api/src/app/InternalAppContext.ts b/packages/core-app-api/src/app/InternalAppContext.ts
new file mode 100644
index 0000000000..81382acf62
--- /dev/null
+++ b/packages/core-app-api/src/app/InternalAppContext.ts
@@ -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);
diff --git a/packages/core-app-api/src/app/index.ts b/packages/core-app-api/src/app/index.ts
index 7843b36339..f6289a5830 100644
--- a/packages/core-app-api/src/app/index.ts
+++ b/packages/core-app-api/src/app/index.ts
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+export { AppRouter } from './AppRouter';
export { createSpecializedApp } from './createSpecializedApp';
export { defaultConfigLoader } from './defaultConfigLoader';
export * from './types';
diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts
index 70022534a1..861766f8d3 100644
--- a/packages/core-app-api/src/app/types.ts
+++ b/packages/core-app-api/src/app/types.ts
@@ -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<{}>;
};