core-api: run bindRoutes in the app impl and inject provider

This commit is contained in:
Fredrik Adelöw
2020-12-07 15:59:37 +01:00
committed by Patrik Oldsberg
parent ad5eab923c
commit 5d52e20a44
5 changed files with 130 additions and 29 deletions
+40
View File
@@ -0,0 +1,40 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { createExternalRouteRef, createRouteRef } from '../routing/RouteRef';
import { generateBoundRoutes } from './App';
describe('generateBoundRoutes', () => {
it('runs happy path', () => {
const external = { myRoute: createExternalRouteRef() };
const ref = createRouteRef({ path: '', title: '' });
const result = generateBoundRoutes(({ bind }) => {
bind(external, { myRoute: ref });
});
expect(result.get(external.myRoute)).toBe(ref);
});
it('throws on unknown keys', () => {
const external = { myRoute: createExternalRouteRef() };
const ref = createRouteRef({ path: '', title: '' });
expect(() =>
generateBoundRoutes(({ bind }) => {
bind(external, { someOtherRoute: ref } as any);
}),
).toThrow('Key someOtherRoute is not an existing external route');
});
});
+87 -28
View File
@@ -15,46 +15,82 @@
*/
import React, {
ComponentType,
PropsWithChildren,
ReactElement,
useMemo,
useState,
ReactElement,
PropsWithChildren,
} from 'react';
import { Route, Routes, Navigate } from 'react-router-dom';
import { AppContextProvider } from './AppContext';
import {
BackstageApp,
AppComponents,
AppConfigLoader,
SignInResult,
SignInPageProps,
} from './types';
import { BackstagePlugin } from '../plugin';
import {
featureFlagsApiRef,
AppThemeApi,
ConfigApi,
identityApiRef,
} from '../apis/definitions';
import { AppThemeProvider } from './AppThemeProvider';
import { IconComponent, SystemIcons, SystemIconKey } from '../icons';
import { Navigate, Route, Routes } from 'react-router-dom';
import { useAsync } from 'react-use';
import {
AnyApiFactory,
ApiHolder,
ApiProvider,
ApiRegistry,
AppTheme,
AppThemeSelector,
appThemeApiRef,
AppThemeSelector,
configApiRef,
ConfigReader,
useApi,
AnyApiFactory,
ApiHolder,
LocalStorageFeatureFlags,
useApi,
} from '../apis';
import { useAsync } from 'react-use';
import {
AppThemeApi,
ConfigApi,
featureFlagsApiRef,
identityApiRef,
} from '../apis/definitions';
import { ApiFactoryRegistry, ApiResolver } from '../apis/system';
import {
childDiscoverer,
routeElementDiscoverer,
traverseElementTree,
} from '../extensions/traversal';
import { IconComponent, SystemIconKey, SystemIcons } from '../icons';
import { BackstagePlugin } from '../plugin';
import { RouteRef } from '../routing';
import {
routeObjectCollector,
routeParentCollector,
routePathCollector,
} from '../routing/collectors';
import { RoutingProvider } from '../routing/hooks';
import { ExternalRouteRef } from '../routing/RouteRef';
import { AppContextProvider } from './AppContext';
import { AppIdentity } from './AppIdentity';
import { ApiResolver, ApiFactoryRegistry } from '../apis/system';
import { AppThemeProvider } from './AppThemeProvider';
import {
AppComponents,
AppConfigLoader,
AppOptions,
BackstageApp,
BindRouteFunc,
SignInPageProps,
SignInResult,
} from './types';
export function generateBoundRoutes(
bindRoutes: AppOptions['bindRoutes'],
): Map<ExternalRouteRef, RouteRef> {
const result = new Map<ExternalRouteRef, RouteRef>();
if (bindRoutes) {
const bind: BindRouteFunc = (externalRoutes, targetRoutes) => {
for (const [key, value] of Object.entries(targetRoutes)) {
const externalRoute = externalRoutes[key];
if (!externalRoute) {
throw new Error(`Key ${key} is not an existing external route`);
}
result.set(externalRoute, value);
}
};
bindRoutes({ bind });
}
return result;
}
type FullAppOptions = {
apis: Iterable<AnyApiFactory>;
@@ -64,6 +100,7 @@ type FullAppOptions = {
themes: AppTheme[];
configLoader?: AppConfigLoader;
defaultApis: Iterable<AnyApiFactory>;
bindRoutes?: AppOptions['bindRoutes'];
};
function useConfigLoader(
@@ -112,6 +149,7 @@ export class PrivateAppImpl implements BackstageApp {
private readonly themes: AppTheme[];
private readonly configLoader?: AppConfigLoader;
private readonly defaultApis: Iterable<AnyApiFactory>;
private readonly bindRoutes: AppOptions['bindRoutes'];
private readonly identityApi = new AppIdentity();
@@ -123,6 +161,7 @@ export class PrivateAppImpl implements BackstageApp {
this.themes = options.themes;
this.configLoader = options.configLoader;
this.defaultApis = options.defaultApis;
this.bindRoutes = options.bindRoutes;
}
getPlugins(): BackstagePlugin<any, any>[] {
@@ -202,6 +241,17 @@ export class PrivateAppImpl implements BackstageApp {
[],
);
// Probably want to memo this?
const { routePaths, routeParents, routeObjects } = traverseElementTree({
root: children,
discoverers: [childDiscoverer, routeElementDiscoverer],
collectors: {
routePaths: routePathCollector,
routeParents: routeParentCollector,
routeObjects: routeObjectCollector,
},
});
const loadedConfig = useConfigLoader(
this.configLoader,
this.components,
@@ -218,7 +268,16 @@ export class PrivateAppImpl implements BackstageApp {
return (
<ApiProvider apis={this.getApiHolder()}>
<AppContextProvider app={this}>
<AppThemeProvider>{children}</AppThemeProvider>
<AppThemeProvider>
<RoutingProvider
routePaths={routePaths}
routeParents={routeParents}
routeObjects={routeObjects}
routeBindings={generateBoundRoutes(this.bindRoutes)}
>
{children}
</RoutingProvider>
</AppThemeProvider>
</AppContextProvider>
</ApiProvider>
);
+1 -1
View File
@@ -79,7 +79,7 @@ export type AppComponents = {
*/
export type AppConfigLoader = () => Promise<AppConfig[]>;
type BindRouteFunc = <T extends AnyExternalRoutes>(
export type BindRouteFunc = <T extends AnyExternalRoutes>(
externalRoutes: T,
targetRoutes: { [key in keyof T]: RouteRef<any> },
) => void;
@@ -142,6 +142,7 @@ export function createApp(options?: AppOptions) {
themes,
configLoader,
defaultApis,
bindRoutes: options?.bindRoutes,
});
app.verify();
@@ -80,6 +80,7 @@ export function wrapInTestApp(
},
],
defaultApis: mockApis,
bindRoutes: () => {},
});
let Wrapper: ComponentType;