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}
+
+
+
+
+
+
);
};