core-app-api: use new basename router prop

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2022-10-17 13:27:17 +02:00
parent ec093b7fb6
commit 9b737e5f2e
5 changed files with 274 additions and 9 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-app-api': minor
---
Updated the React Router wiring to make use of the new `basename` property of the router components in React Router v6 stable. To implement this, a new optional `basename` property has been added to the `Router` app component, which can be forwarded to the concrete router implementation in order to support this new behavior. This is done by default in any app that does not have a `Router` component override.
@@ -0,0 +1,107 @@
/*
* Copyright 2020 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 tlr, { render } from '@testing-library/react';
import React from 'react';
describe.each(['beta', 'stable'])('react-router %s', rrVersion => {
beforeAll(() => {
jest.doMock('react', () => React);
// This has some side effects, so need this to be stable to avoid re-require
jest.doMock('@testing-library/react', () => tlr);
jest.doMock('react-router', () =>
rrVersion === 'beta'
? jest.requireActual('react-router-beta')
: jest.requireActual('react-router-stable'),
);
jest.doMock('react-router-dom', () =>
rrVersion === 'beta'
? jest.requireActual('react-router-dom-beta')
: jest.requireActual('react-router-dom-stable'),
);
});
afterAll(() => {
jest.resetModules();
});
function requireDeps() {
return {
...(require('./AppManager') as typeof import('./AppManager')),
...(require('../routing') as typeof import('../routing')),
...(require('react-router-dom') as typeof import('react-router-dom')),
...(require('@backstage/test-utils') as typeof import('@backstage/test-utils')),
};
}
describe('AppManager', () => {
it('supports base path', async () => {
const { AppManager, MemoryRouter, Navigate, Route, FlatRoutes } =
requireDeps();
const app = new AppManager({
apis: [],
defaultApis: [],
themes: [
{
id: 'light',
title: 'Light Theme',
variant: 'light',
Provider: ({ children }) => <>{children}</>,
},
],
icons: {} as any,
plugins: [],
components: {
NotFoundErrorPage: () => null,
BootErrorPage: () => null,
Progress: () => null,
Router: ({ children, basename }) => (
<MemoryRouter
initialEntries={['/foo']}
basename={basename}
children={children}
/>
),
ErrorBoundaryFallback: () => null,
ThemeProvider: ({ children }) => <>{children}</>,
},
configLoader: async () => [
{
context: 'test',
data: { app: { baseUrl: 'http://localhost/foo' } },
},
],
bindRoutes: () => {},
});
const AppProvider = app.getProvider();
const AppRouter = app.getRouter();
const rendered = render(
<AppProvider>
<AppRouter>
<FlatRoutes>
<Route path="/" element={<Navigate to="bar" />} />
<Route path="/bar" element={<span>bar</span>} />
</FlatRoutes>
</AppRouter>
</AppProvider>,
);
await expect(rendered.findByText('bar')).resolves.toBeInTheDocument();
});
});
});
@@ -0,0 +1,118 @@
/*
* Copyright 2020 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 { render } from '@testing-library/react';
import React from 'react';
import { MemoryRouter, Navigate, Route } from 'react-router-dom';
import { FlatRoutes } from '../routing';
import { AppManager } from './AppManager';
import { AppOptions } from './types';
jest.mock('react-router', () => jest.requireActual('react-router-stable'));
jest.mock('react-router-dom', () =>
jest.requireActual('react-router-dom-stable'),
);
const mockAppOptions: AppOptions = {
apis: [],
defaultApis: [],
themes: [
{
id: 'light',
title: 'Light Theme',
variant: 'light',
Provider: ({ children }) => <>{children}</>,
},
],
icons: {} as any,
plugins: [],
components: {
NotFoundErrorPage: () => null,
BootErrorPage: () => null,
Progress: () => null,
Router: props => <MemoryRouter {...props} />,
ErrorBoundaryFallback: () => null,
ThemeProvider: ({ children }) => <>{children}</>,
},
configLoader: async () => [],
bindRoutes: () => {},
};
describe('AppManager', () => {
it('supports base path', async () => {
const app = new AppManager({
...mockAppOptions,
components: {
...mockAppOptions.components,
Router: props => <MemoryRouter {...props} initialEntries={['/foo']} />,
},
configLoader: async () => [
{
context: 'test',
data: { app: { baseUrl: 'http://localhost/foo' } },
},
],
});
const AppProvider = app.getProvider();
const AppRouter = app.getRouter();
const rendered = render(
<AppProvider>
<AppRouter>
<FlatRoutes>
<Route path="/" element={<Navigate to="bar" />} />
<Route path="/bar" element={<span>bar</span>} />
</FlatRoutes>
</AppRouter>
</AppProvider>,
);
await expect(rendered.findByText('bar')).resolves.toBeInTheDocument();
});
it('supports base path with absolute navigation', async () => {
const app = new AppManager({
...mockAppOptions,
components: {
...mockAppOptions.components,
Router: props => <MemoryRouter {...props} initialEntries={['/foo']} />,
},
configLoader: async () => [
{
context: 'test',
data: { app: { baseUrl: 'http://localhost/foo' } },
},
],
});
const AppProvider = app.getProvider();
const AppRouter = app.getRouter();
const rendered = render(
<AppProvider>
<AppRouter>
<FlatRoutes>
<Route path="/" element={<Navigate to="/bar" />} />
<Route path="/bar" element={<span>bar</span>} />
</FlatRoutes>
</AppRouter>
</AppProvider>,
);
await expect(rendered.findByText('bar')).resolves.toBeInTheDocument();
});
});
+43 -8
View File
@@ -99,6 +99,21 @@ const InternalAppContext = createContext<{
* 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://dummy.dev', // baseUrl can be specified as just a path
@@ -361,7 +376,7 @@ export class AppManager implements BackstageApp {
const AppRouter = ({ children }: PropsWithChildren<{}>) => {
const configApi = useApi(configApiRef);
const basePath = getBasePath(configApi);
const basePath = readBasePath(configApi);
const mountPath = `${basePath}/*`;
const { routeObjects } = useContext(InternalAppContext);
@@ -390,23 +405,43 @@ export class AppManager implements BackstageApp {
{ 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} />
<Routes>
<Route path={mountPath} element={<>{children}</>} />
</Routes>
<SignInPageWrapper component={SignInPageComponent}>
<Routes>
<Route path={mountPath} element={<>{children}</>} />
</Routes>
</SignInPageWrapper>
</RouterComponent>
);
}
return (
<RouterComponent>
<RouterComponent basename={basePath}>
<RouteTracker routeObjects={routeObjects} />
<SignInPageWrapper component={SignInPageComponent}>
<Routes>
<Route path={mountPath} element={<>{children}</>} />
</Routes>
<>{children}</>
</SignInPageWrapper>
</RouterComponent>
);
+1 -1
View File
@@ -69,7 +69,7 @@ export type AppComponents = {
NotFoundErrorPage: ComponentType<{}>;
BootErrorPage: ComponentType<BootErrorPageProps>;
Progress: ComponentType<{}>;
Router: ComponentType<{}>;
Router: ComponentType<{ basename?: string }>;
ErrorBoundaryFallback: ComponentType<ErrorBoundaryFallbackProps>;
ThemeProvider?: ComponentType<{}>;