packages/core: refactor createApp to just take options + separate out AppContext
This commit is contained in:
+21
-16
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<{}>;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user