core-app-api: use new basename router prop
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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<{}>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user