diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index d915411987..dd4bf42c2c 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -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 ( - - - - - - - - - - - - + + + + + + + + + + + + + + ); }; diff --git a/packages/core/src/api/app/AppBuilder.tsx b/packages/core/src/api/app/App.tsx similarity index 61% rename from packages/core/src/api/app/AppBuilder.tsx rename to packages/core/src/api/app/App.tsx index a375593cdb..c44454b10e 100644 --- a/packages/core/src/api/app/AppBuilder.tsx +++ b/packages/core/src/api/app/App.tsx @@ -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(); - - registerApis(apis: ApiHolder) { - this.apis = apis; + return this.icons[key]; } - registerIcons(icons: Partial) { - 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(); const registeredFeatureFlags = new Array(); + 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 = ( {routes} - ( - - )} - /> + ); @@ -142,10 +140,48 @@ export class AppBuilder { rendered = ; } - return () => ; + return () => rendered; + } + + getProvider(): ComponentType<{}> { + const Provider: FC<{}> = ({ children }) => ( + {children} + ); + return Provider; + } + + verify() { + const pluginIds = new Set(); + + 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 = () => ( + + ); + + 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; } diff --git a/packages/core/src/api/app/AppContext.tsx b/packages/core/src/api/app/AppContext.tsx index c3077801e6..ec3831992f 100644 --- a/packages/core/src/api/app/AppContext.tsx +++ b/packages/core/src/api/app/AppContext.tsx @@ -15,19 +15,19 @@ */ import React, { createContext, useContext, FC } from 'react'; -import { App } from './types'; +import { BackstageApp } from './types'; -const Context = createContext(undefined); +const Context = createContext(undefined); type Props = { - app: App; + app: BackstageApp; }; export const AppContextProvider: FC = ({ app, children }) => ( ); -export const useApp = (): App => { +export const useApp = (): BackstageApp => { const app = useContext(Context); if (!app) { throw new Error('No app context available'); diff --git a/packages/core/src/api/app/index.ts b/packages/core/src/api/app/index.ts index 156c9870b8..c40fa9e9f8 100644 --- a/packages/core/src/api/app/index.ts +++ b/packages/core/src/api/app/index.ts @@ -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'; diff --git a/packages/core/src/api/app/types.ts b/packages/core/src/api/app/types.ts index 4a6efe66fb..e8d597e120 100644 --- a/packages/core/src/api/app/types.ts +++ b/packages/core/src/api/app/types.ts @@ -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 { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - build(_app: App): ComponentType { - 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; + + /** + * A list of all plugins to include in the app. + */ + plugins?: BackstagePlugin[]; + + /** + * Supply components to the app to override the default ones. + */ + components?: Partial; +}; + +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<{}>; +}; diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index e68dac4021..3a69faa18f 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -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 ( - - - - - {sidebar} - - - - - + + + + + + {sidebar} + + + + + + ); };