packages/core: refactor createApp to just take options + separate out AppContext

This commit is contained in:
Patrik Oldsberg
2020-05-14 15:36:26 +02:00
parent 299589040f
commit ce55e67f31
6 changed files with 177 additions and 84 deletions
+21 -16
View File
@@ -25,10 +25,13 @@ import * as plugins from './plugins';
import apis, { alertApiForwarder } from './apis';
import { ThemeContextType, ThemeContext, useThemeType } from './ThemeContext';
const app = createApp();
app.registerApis(apis);
app.registerPlugin(...Object.values(plugins));
const AppComponent = app.build();
const app = createApp({
apis,
plugins: Object.values(plugins),
});
const AppProvider = app.getProvider();
const AppComponent = app.getRootComponent();
const App: FC<{}> = () => {
const [theme, toggleTheme] = useThemeType(
@@ -59,18 +62,20 @@ const App: FC<{}> = () => {
toggleTheme,
};
return (
<ThemeContext.Provider value={themeContext}>
<ThemeProvider theme={backstageTheme}>
<CssBaseline>
<AlertDisplay forwarder={alertApiForwarder} />
<Router>
<Root>
<AppComponent />
</Root>
</Router>
</CssBaseline>
</ThemeProvider>
</ThemeContext.Provider>
<AppProvider>
<ThemeContext.Provider value={themeContext}>
<ThemeProvider theme={backstageTheme}>
<CssBaseline>
<AlertDisplay forwarder={alertApiForwarder} />
<Router>
<Root>
<AppComponent />
</Root>
</Router>
</CssBaseline>
</ThemeProvider>
</ThemeContext.Provider>
</AppProvider>
);
};
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import React, { ComponentType } from 'react';
import React, { ComponentType, FC } from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import { AppContextProvider } from './AppContext';
import { App } from './types';
import { BackstageApp, AppOptions, AppComponents } from './types';
import { BackstagePlugin } from '../plugin';
import { FeatureFlagsRegistryItem } from './FeatureFlags';
import { featureFlagsApiRef } from '../apis/definitions';
@@ -29,45 +29,47 @@ import {
SystemIconKey,
defaultSystemIcons,
} from '../../icons';
import { ApiHolder, ApiProvider } from '../apis';
import { ApiHolder, ApiProvider, ApiRegistry } from '../apis';
import LoginPage from './LoginPage';
class AppImpl implements App {
constructor(private readonly systemIcons: SystemIcons) {}
type FullAppOptions = {
apis: ApiHolder;
icons: SystemIcons;
plugins: BackstagePlugin[];
components: AppComponents;
};
class AppImpl implements BackstageApp {
private readonly apis: ApiHolder;
private readonly icons: SystemIcons;
private readonly plugins: BackstagePlugin[];
private readonly components: AppComponents;
constructor(options: FullAppOptions) {
this.apis = options.apis;
this.icons = options.icons;
this.plugins = options.plugins;
this.components = options.components;
}
getApis(): ApiHolder {
return this.apis;
}
getPlugins(): BackstagePlugin[] {
return this.plugins;
}
getSystemIcon(key: SystemIconKey): IconComponent {
return this.systemIcons[key];
}
}
export class AppBuilder {
private apis?: ApiHolder;
private systemIcons = { ...defaultSystemIcons };
private readonly plugins = new Set<BackstagePlugin>();
registerApis(apis: ApiHolder) {
this.apis = apis;
return this.icons[key];
}
registerIcons(icons: Partial<SystemIcons>) {
this.systemIcons = { ...this.systemIcons, ...icons };
}
registerPlugin(...plugin: BackstagePlugin[]) {
for (const p of plugin) {
if (this.plugins.has(p)) {
throw new Error(`Plugin '${p}' is already registered`);
}
this.plugins.add(p);
}
}
build(): ComponentType<{}> {
const app = new AppImpl(this.systemIcons);
getRootComponent(): ComponentType<{}> {
const routes = new Array<JSX.Element>();
const registeredFeatureFlags = new Array<FeatureFlagsRegistryItem>();
const { NotFoundErrorPage } = this.components;
for (const plugin of this.plugins.values()) {
for (const output of plugin.output()) {
switch (output.type) {
@@ -130,11 +132,7 @@ export class AppBuilder {
let rendered = (
<Switch>
{routes}
<Route
render={(props) => (
<ErrorPage {...props} status="404" statusMessage="PAGE NOT FOUND" />
)}
/>
<Route component={NotFoundErrorPage} />
</Switch>
);
@@ -142,10 +140,48 @@ export class AppBuilder {
rendered = <ApiProvider apis={this.apis} children={rendered} />;
}
return () => <AppContextProvider app={app} children={rendered} />;
return () => rendered;
}
getProvider(): ComponentType<{}> {
const Provider: FC<{}> = ({ children }) => (
<AppContextProvider app={this}>{children}</AppContextProvider>
);
return Provider;
}
verify() {
const pluginIds = new Set<string>();
for (const plugin of this.plugins) {
const id = plugin.getId();
if (pluginIds.has(id)) {
throw new Error(`Duplicate plugin found '${id}'`);
}
pluginIds.add(id);
}
}
}
export function createApp() {
return new AppBuilder();
/**
* Creates a new Backstage App.
*/
export function createApp(options?: AppOptions) {
const DefaultNotFoundPage = () => (
<ErrorPage status="404" statusMessage="PAGE NOT FOUND" />
);
const apis = options?.apis ?? ApiRegistry.from([]);
const icons = { ...defaultSystemIcons, ...options?.icons };
const plugins = options?.plugins ?? [];
const components = {
NotFoundErrorPage: DefaultNotFoundPage,
...options?.components,
};
const app = new AppImpl({ apis, icons, plugins, components });
app.verify();
return app;
}
+4 -4
View File
@@ -15,19 +15,19 @@
*/
import React, { createContext, useContext, FC } from 'react';
import { App } from './types';
import { BackstageApp } from './types';
const Context = createContext<App | undefined>(undefined);
const Context = createContext<BackstageApp | undefined>(undefined);
type Props = {
app: App;
app: BackstageApp;
};
export const AppContextProvider: FC<Props> = ({ app, children }) => (
<Context.Provider value={app} children={children} />
);
export const useApp = (): App => {
export const useApp = (): BackstageApp => {
const app = useContext(Context);
if (!app) {
throw new Error('No app context available');
+1 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
export { createApp } from './AppBuilder';
export { createApp } from './App';
export { FeatureFlags } from './FeatureFlags';
export { useApp } from './AppContext';
export * from './types';
+57 -9
View File
@@ -15,15 +15,63 @@
*/
import { ComponentType } from 'react';
import { IconComponent, SystemIconKey } from '../../icons';
import { IconComponent, SystemIconKey, SystemIcons } from '../../icons';
import { BackstagePlugin } from '../plugin';
import { ApiHolder } from '../apis';
export type App = {
getSystemIcon(key: SystemIconKey): IconComponent;
export type AppComponents = {
NotFoundErrorPage: ComponentType<{}>;
};
export class AppComponentBuilder<T = any> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
build(_app: App): ComponentType<T> {
throw new Error('Must override build() in AppComponentBuilder');
}
}
export type AppOptions = {
/**
* A holder of all APIs available in the app.
*
* Use for example ApiRegistry or ApiTestRegistry.
*/
apis?: ApiHolder;
/**
* Supply icons to override the default ones.
*/
icons?: Partial<SystemIcons>;
/**
* A list of all plugins to include in the app.
*/
plugins?: BackstagePlugin[];
/**
* Supply components to the app to override the default ones.
*/
components?: Partial<AppComponents>;
};
export type BackstageApp = {
/**
* Get the holder for all APIs available in the app.
*/
getApis(): ApiHolder;
/**
* Returns all plugins registered for the app.
*/
getPlugins(): BackstagePlugin[];
/**
* Get a common icon for this app.
*/
getSystemIcon(key: SystemIconKey): IconComponent;
/**
* Creates a root component for this app, including the App chrome
* and routes to all plugins.
*/
getRootComponent(): ComponentType<{}>;
/**
* Provider component that should wrap the App's RootComponent and
* any other components that need to be within the app context.
*/
getProvider(): ComponentType<{}>;
};
+18 -14
View File
@@ -66,25 +66,29 @@ class DevAppBuilder {
* Build a DevApp component using the resources registered so far
*/
build(): ComponentType<{}> {
const app = createApp();
app.registerApis(this.setupApiRegistry(this.factories));
app.registerPlugin(...this.plugins);
const AppComponent = app.build();
const app = createApp({
apis: this.setupApiRegistry(this.factories),
plugins: this.plugins,
});
const AppProvider = app.getProvider();
const AppComponent = app.getRootComponent();
const sidebar = this.setupSidebar(this.plugins);
const DevApp: FC<{}> = () => {
return (
<ThemeProvider theme={lightTheme}>
<CssBaseline>
<BrowserRouter>
<SidebarPage>
{sidebar}
<AppComponent />
</SidebarPage>
</BrowserRouter>
</CssBaseline>
</ThemeProvider>
<AppProvider>
<ThemeProvider theme={lightTheme}>
<CssBaseline>
<BrowserRouter>
<SidebarPage>
{sidebar}
<AppComponent />
</SidebarPage>
</BrowserRouter>
</CssBaseline>
</ThemeProvider>
</AppProvider>
);
};