diff --git a/frontend/packages/app/src/entities/index.ts b/frontend/packages/app/src/entities/index.ts index 47c55a3c57..f32f99ab06 100644 --- a/frontend/packages/app/src/entities/index.ts +++ b/frontend/packages/app/src/entities/index.ts @@ -1,6 +1,6 @@ import { createEntityKind, - createOverviewPage, + createWidgetView, createEntityView, } from '@backstage/core'; import ComputerIcon from '@material-ui/icons/Computer'; @@ -8,7 +8,7 @@ import MockEntityPage from './MockEntityPage'; import MockEntityCard from './MockEntityCard'; /* SERVICE */ -const serviceOverviewPage = createOverviewPage() +const serviceOverviewPage = createWidgetView() .addComponent(MockEntityCard) .addComponent(MockEntityCard); diff --git a/frontend/packages/core/src/api/api.ts b/frontend/packages/core/src/api/api.ts new file mode 100644 index 0000000000..73ca5c1da3 --- /dev/null +++ b/frontend/packages/core/src/api/api.ts @@ -0,0 +1,25 @@ +import AppBuilder from './app/AppBuilder'; +import EntityKind, { EntityConfig } from './entity/EntityKind'; +import WidgetViewBuilder from './widgetView/WidgetViewBuilder'; +import EntityViewBuilder from './entityView/EntityViewPageBuilder'; +import BackstagePlugin, { PluginConfig } from './plugin/Plugin'; + +export function createApp() { + return new AppBuilder(); +} + +export function createEntityKind(config: EntityConfig) { + return new EntityKind(config); +} + +export function createWidgetView() { + return new WidgetViewBuilder(); +} + +export function createEntityView() { + return new EntityViewBuilder(); +} + +export function createPlugin(config: PluginConfig): BackstagePlugin { + return new BackstagePlugin(config); +} diff --git a/frontend/packages/core/src/appApi/AppBuilder.tsx b/frontend/packages/core/src/api/app/AppBuilder.tsx similarity index 67% rename from frontend/packages/core/src/appApi/AppBuilder.tsx rename to frontend/packages/core/src/api/app/AppBuilder.tsx index ec61b20a86..349d9a7b06 100644 --- a/frontend/packages/core/src/appApi/AppBuilder.tsx +++ b/frontend/packages/core/src/api/app/AppBuilder.tsx @@ -1,10 +1,10 @@ import React, { ComponentType, FC } from 'react'; +import { Route, Switch, useParams } from 'react-router-dom'; import { AppContextProvider } from './AppContext'; -import { App, EntityConfig, AppComponentBuilder } from './types'; -import { Route, Switch, useParams, Redirect } from 'react-router-dom'; -import EntityKind from './EntityKind'; -import { EntityContextProvider } from './EntityContext'; -import { BackstagePlugin } from './types'; +import { App, AppComponentBuilder } from './types'; +import EntityKind, { EntityConfig } from '../entity/EntityKind'; +import { EntityContextProvider } from '../entityView/EntityContext'; +import BackstagePlugin, { registerSymbol } from '../plugin/Plugin'; class AppImpl implements App { constructor(private readonly entities: Map) {} @@ -30,7 +30,7 @@ function builtComponent( export default class AppBuilder { private readonly entities = new Map(); - private readonly plugins = new Map(); + private readonly plugins = new Set(); registerEntityKind(...entity: EntityKind[]) { for (const e of entity) { @@ -44,11 +44,10 @@ export default class AppBuilder { registerPlugin(...plugin: BackstagePlugin[]) { for (const p of plugin) { - const { id } = p; - if (this.plugins.has(id)) { - throw new Error(`Plugin '${id}' is already registered`); + if (this.plugins.has(p)) { + throw new Error(`Plugin '${p}' is already registered`); } - this.plugins.set(id, p); + this.plugins.add(p); } } @@ -98,30 +97,8 @@ export default class AppBuilder { const pluginRoutes = new Array(); for (const plugin of this.plugins.values()) { - plugin.register({ - router: { - registerRoute(path, component, options = {}) { - if (path.startsWith('/entity/')) { - throw new Error( - `Plugin ${plugin.id} tried to register forbidden route ${path}`, - ); - } - pluginRoutes.push( - , - ); - }, - registerRedirect(path, target, options = {}) { - if (path.startsWith('/entity/')) { - throw new Error( - `Plugin ${plugin.id} tried to register forbidden redirect ${path}`, - ); - } - pluginRoutes.push( - , - ); - }, - }, - }); + const { routes = [] } = plugin[registerSymbol](); + pluginRoutes.push(...routes); } const routes = [...pluginRoutes, ...entityRoutes]; diff --git a/frontend/packages/core/src/appApi/AppContext.tsx b/frontend/packages/core/src/api/app/AppContext.tsx similarity index 100% rename from frontend/packages/core/src/appApi/AppContext.tsx rename to frontend/packages/core/src/api/app/AppContext.tsx diff --git a/frontend/packages/core/src/api/app/types.ts b/frontend/packages/core/src/api/app/types.ts new file mode 100644 index 0000000000..189ce208e1 --- /dev/null +++ b/frontend/packages/core/src/api/app/types.ts @@ -0,0 +1,12 @@ +import { ComponentType } from 'react'; +import { EntityConfig } from '../entity/EntityKind'; + +export type App = { + getEntityConfig(kind: string): EntityConfig; +}; + +export class AppComponentBuilder { + build(_app: App): ComponentType { + throw new Error('Must override build() in AppComponentBuilder'); + } +} diff --git a/frontend/packages/core/src/api/entity/EntityKind.ts b/frontend/packages/core/src/api/entity/EntityKind.ts new file mode 100644 index 0000000000..7f6447f4a6 --- /dev/null +++ b/frontend/packages/core/src/api/entity/EntityKind.ts @@ -0,0 +1,20 @@ +import { ComponentType } from 'react'; +import { AppComponentBuilder } from '../app/types'; + +export type EntityConfig = { + kind: string; + title: string; + icon: React.ComponentType<{ fontSize: number }>; + color: { + primary: string; + secondary: string; + }; + pages: { + list?: ComponentType<{}> | AppComponentBuilder; + view?: ComponentType<{}> | AppComponentBuilder; + }; +}; + +export default class EntityKind { + constructor(readonly config: EntityConfig) {} +} diff --git a/frontend/packages/core/src/appApi/EntityContext.tsx b/frontend/packages/core/src/api/entityView/EntityContext.tsx similarity index 95% rename from frontend/packages/core/src/appApi/EntityContext.tsx rename to frontend/packages/core/src/api/entityView/EntityContext.tsx index 5efe917f4d..0a229fd1b3 100644 --- a/frontend/packages/core/src/appApi/EntityContext.tsx +++ b/frontend/packages/core/src/api/entityView/EntityContext.tsx @@ -1,5 +1,5 @@ import React, { createContext, useContext, FC } from 'react'; -import { EntityConfig } from './types'; +import { EntityConfig } from '../entity/EntityKind'; type Value = { config: EntityConfig; diff --git a/frontend/packages/core/src/appApi/EntityViewPageBuilder.tsx b/frontend/packages/core/src/api/entityView/EntityViewPageBuilder.tsx similarity index 96% rename from frontend/packages/core/src/appApi/EntityViewPageBuilder.tsx rename to frontend/packages/core/src/api/entityView/EntityViewPageBuilder.tsx index b323cd9504..10985325fc 100644 --- a/frontend/packages/core/src/appApi/EntityViewPageBuilder.tsx +++ b/frontend/packages/core/src/api/entityView/EntityViewPageBuilder.tsx @@ -1,10 +1,10 @@ import React, { ComponentType, FC } from 'react'; -import { AppComponentBuilder, App } from './types'; -import { useEntity, useEntityUri, useEntityConfig } from './EntityContext'; import { Route, Redirect, Switch } from 'react-router-dom'; import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; -import EntityLink from './EntityLink'; +import { AppComponentBuilder, App } from '../app/types'; +import { useEntity, useEntityUri, useEntityConfig } from './EntityContext'; +import EntityLink from '../../components/EntityLink/EntityLink'; const EntityLayout: FC<{}> = ({ children }) => { const config = useEntityConfig(); diff --git a/frontend/packages/core/src/api/index.ts b/frontend/packages/core/src/api/index.ts new file mode 100644 index 0000000000..7852cf8f75 --- /dev/null +++ b/frontend/packages/core/src/api/index.ts @@ -0,0 +1,8 @@ +export * from './types'; +export * from './api'; +export { useApp } from './app/AppContext'; +export { + useEntity, + useEntityConfig, + useEntityUri, +} from './entityView/EntityContext'; diff --git a/frontend/packages/core/src/api/plugin/Plugin.tsx b/frontend/packages/core/src/api/plugin/Plugin.tsx new file mode 100644 index 0000000000..13caa40475 --- /dev/null +++ b/frontend/packages/core/src/api/plugin/Plugin.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { Route, Redirect } from 'react-router-dom'; + +export type PluginConfig = { + id: string; + register?(hooks: PluginHooks): void; +}; + +export type PluginHooks = { + router: Router; +}; + +export type RouteOptions = { + // Whether the route path must match exactly, defaults to true. + exact?: boolean; +}; + +export type RedirectOptions = { + // Whether the route path must match exactly, defaults to true. + exact?: boolean; +}; + +export type Router = { + registerRoute( + path: string, + Component: React.ComponentType, + options?: RouteOptions, + ): void; + registerRedirect( + path: string, + target: string, + options?: RedirectOptions, + ): void; +}; + +export type PluginRegistrationResult = { + routes?: JSX.Element[]; +}; + +export const registerSymbol = Symbol('plugin-register'); + +export default class Plugin { + private result?: PluginRegistrationResult; + + constructor(private readonly config: PluginConfig) {} + + [registerSymbol](): PluginRegistrationResult { + if (this.result) { + return this.result; + } + if (!this.config.register) { + return {}; + } + + const { id } = this.config; + + const routes = new Array(); + + this.config.register({ + router: { + registerRoute(path, component, options = {}) { + if (path.startsWith('/entity/')) { + throw new Error( + `Plugin ${id} tried to register forbidden route ${path}`, + ); + } + const { exact = true } = options; + routes.push( + , + ); + }, + registerRedirect(path, target, options = {}) { + if (path.startsWith('/entity/')) { + throw new Error( + `Plugin ${id} tried to register forbidden redirect ${path}`, + ); + } + const { exact = true } = options; + routes.push( + , + ); + }, + }, + }); + + this.result = { routes }; + return this.result; + } + + toString() { + return `plugin{${this.config.id}}`; + } +} diff --git a/frontend/packages/core/src/api/types.ts b/frontend/packages/core/src/api/types.ts new file mode 100644 index 0000000000..06f1570900 --- /dev/null +++ b/frontend/packages/core/src/api/types.ts @@ -0,0 +1,10 @@ +export type User = { + id: string; + email: string; +}; + +export type UserApi = { + isLoggedIn(): Promise; + + getUser(): Promise; +}; diff --git a/frontend/packages/core/src/appApi/OverviewPageBuilder.tsx b/frontend/packages/core/src/api/widgetView/WidgetViewBuilder.tsx similarity index 58% rename from frontend/packages/core/src/appApi/OverviewPageBuilder.tsx rename to frontend/packages/core/src/api/widgetView/WidgetViewBuilder.tsx index 67a0138436..eb9f5d1ed0 100644 --- a/frontend/packages/core/src/appApi/OverviewPageBuilder.tsx +++ b/frontend/packages/core/src/api/widgetView/WidgetViewBuilder.tsx @@ -1,12 +1,12 @@ import React, { ComponentType, FC } from 'react'; -import { App, AppComponentBuilder } from './types'; +import { App, AppComponentBuilder } from '../app/types'; type Props = { app: App; cards: ComponentType[]; }; -const OverviewPageComponent: FC = ({ cards }) => { +const WidgetViewComponent: FC = ({ cards }) => { return (
{cards.map(CardComponent => ( @@ -16,16 +16,16 @@ const OverviewPageComponent: FC = ({ cards }) => { ); }; -type OverviewPageRegistration = { +type WidgetViewRegistration = { type: 'component'; component: ComponentType; }; -export default class OverviewPageBuilder extends AppComponentBuilder { - private readonly registrations = new Array(); +export default class WidgetViewBuilder extends AppComponentBuilder { + private readonly registrations = new Array(); private output?: ComponentType; - addComponent(component: ComponentType): OverviewPageBuilder { + addComponent(component: ComponentType): WidgetViewBuilder { this.registrations.push({ type: 'component', component }); return this; } @@ -40,11 +40,11 @@ export default class OverviewPageBuilder extends AppComponentBuilder { case 'component': return reg.component; default: - throw new Error(`Unknown OverviewPageBuilder registration`); + throw new Error(`Unknown WidgetViewBuilder registration`); } }); - this.output = () => ; + this.output = () => ; return this.output; } } diff --git a/frontend/packages/core/src/appApi/EntityKind.ts b/frontend/packages/core/src/appApi/EntityKind.ts deleted file mode 100644 index 058fdcef6d..0000000000 --- a/frontend/packages/core/src/appApi/EntityKind.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { EntityConfig } from './types'; - -export default class EntityKind { - constructor(readonly config: EntityConfig) {} -} diff --git a/frontend/packages/core/src/appApi/api.ts b/frontend/packages/core/src/appApi/api.ts deleted file mode 100644 index f3eaa0692b..0000000000 --- a/frontend/packages/core/src/appApi/api.ts +++ /dev/null @@ -1,25 +0,0 @@ -import AppBuilder from './AppBuilder'; -import { EntityConfig, PluginConfig, BackstagePlugin } from './types'; -import EntityKind from './EntityKind'; -import OverviewPageBuilder from './OverviewPageBuilder'; -import EntityViewBuilder from './EntityViewPageBuilder'; - -export function createApp() { - return new AppBuilder(); -} - -export function createEntityKind(config: EntityConfig) { - return new EntityKind(config); -} - -export function createOverviewPage() { - return new OverviewPageBuilder(); -} - -export function createEntityView() { - return new EntityViewBuilder(); -} - -export function createPlugin(config: PluginConfig): BackstagePlugin { - return { register() {}, ...config }; -} diff --git a/frontend/packages/core/src/appApi/createPlugin.test.ts b/frontend/packages/core/src/appApi/createPlugin.test.ts deleted file mode 100644 index edda12f09e..0000000000 --- a/frontend/packages/core/src/appApi/createPlugin.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import createPlugin from './createPlugin'; - -describe('createPlugin', () => { - it('should create a plugin', () => { - const plugin = createPlugin({ id: 'my-plugin' }); - expect(plugin.id).toBe('my-plugin'); - }); -}); diff --git a/frontend/packages/core/src/appApi/createPlugin.ts b/frontend/packages/core/src/appApi/createPlugin.ts deleted file mode 100644 index cc5f4a6090..0000000000 --- a/frontend/packages/core/src/appApi/createPlugin.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { BackstagePlugin, PluginConfig } from './types'; - -function createPlugin(config: PluginConfig): BackstagePlugin { - return { - register() {}, - ...config, - }; -} - -export default createPlugin; diff --git a/frontend/packages/core/src/appApi/index.ts b/frontend/packages/core/src/appApi/index.ts deleted file mode 100644 index d198cb66e4..0000000000 --- a/frontend/packages/core/src/appApi/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './types'; -export * from './api'; -export { useApp } from './AppContext'; -export { useEntity, useEntityConfig, useEntityUri } from './EntityContext'; -export { default as EntityLink } from './EntityLink'; diff --git a/frontend/packages/core/src/appApi/types.ts b/frontend/packages/core/src/appApi/types.ts deleted file mode 100644 index d33d6d5e40..0000000000 --- a/frontend/packages/core/src/appApi/types.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { ComponentType } from 'react'; - -export type EntityConfig = { - kind: string; - title: string; - icon: React.ComponentType<{ fontSize: number }>; - color: { - primary: string; - secondary: string; - }; - pages: { - list?: ComponentType<{}> | AppComponentBuilder; - view?: ComponentType<{}> | AppComponentBuilder; - }; -}; - -export type App = { - getEntityConfig(kind: string): EntityConfig; -}; - -export class AppComponentBuilder { - build(_app: App): ComponentType { - throw new Error('Must override build() in AppComponentBuilder'); - } -} - -export type User = { - id: string; - email: string; -}; - -export type UserApi = { - isLoggedIn(): Promise; - - getUser(): Promise; -}; - -export type PluginConfig = { - id: string; - register?(hooks: PluginHooks): void; -}; - -export interface BackstagePlugin { - id: string; - register(hooks: PluginHooks): void; -} - -export type PluginHooks = { - router: Router; -}; - -export type RouteOptions = { - // Whether the route path must match exactly, defaults to true. - exact?: boolean; -}; - -export type RedirectOptions = { - // Whether the route path must match exactly, defaults to true. - exact?: boolean; -}; - -export type Router = { - registerRoute( - path: string, - Component: React.ComponentType, - options?: RouteOptions, - ): void; - registerRedirect( - path: string, - target: string, - options?: RedirectOptions, - ): void; -}; diff --git a/frontend/packages/core/src/appApi/EntityLink.tsx b/frontend/packages/core/src/components/EntityLink/EntityLink.tsx similarity index 100% rename from frontend/packages/core/src/appApi/EntityLink.tsx rename to frontend/packages/core/src/components/EntityLink/EntityLink.tsx diff --git a/frontend/packages/core/src/components/EntityLink/index.ts b/frontend/packages/core/src/components/EntityLink/index.ts new file mode 100644 index 0000000000..d35b5bee26 --- /dev/null +++ b/frontend/packages/core/src/components/EntityLink/index.ts @@ -0,0 +1 @@ +export { default } from './EntityLink'; diff --git a/frontend/packages/core/src/index.ts b/frontend/packages/core/src/index.ts index 2e08a8929e..3dc9c80d41 100644 --- a/frontend/packages/core/src/index.ts +++ b/frontend/packages/core/src/index.ts @@ -1,9 +1,9 @@ -export * from './appApi'; +export * from './api'; +export { default as EntityLink } from './components/EntityLink'; export { default as Page } from '../src/layout/Page'; export { gradients, theme } from '../src/layout/Page'; export { default as Header } from '../src/layout/Header/Header'; export { default as HeaderLabel } from '../src/layout/HeaderLabel'; export { default as InfoCard } from '../src/layout/InfoCard'; export { default as ErrorBoundary } from '../src/layout/ErrorBoundary'; - export { default as BackstageTheme } from '../src/theme/BackstageTheme'; diff --git a/frontend/packages/plugins/_template/{{ cookiecutter.plugin_name }}/src/plugin.test.ts b/frontend/packages/plugins/_template/{{ cookiecutter.plugin_name }}/src/plugin.test.ts index 869fa6f67f..670ca54e93 100644 --- a/frontend/packages/plugins/_template/{{ cookiecutter.plugin_name }}/src/plugin.test.ts +++ b/frontend/packages/plugins/_template/{{ cookiecutter.plugin_name }}/src/plugin.test.ts @@ -2,6 +2,6 @@ import plugin from './plugin'; describe('{{ cookiecutter.plugin_name }}', () => { it('should export plugin', () => { - expect(plugin.id).toBe('{{ cookiecutter.plugin_name }}'); + expect(plugin).toBeDefined(); }); }); diff --git a/frontend/packages/plugins/hello-world/src/plugin.test.ts b/frontend/packages/plugins/hello-world/src/plugin.test.ts index af8c878dad..6335998b14 100644 --- a/frontend/packages/plugins/hello-world/src/plugin.test.ts +++ b/frontend/packages/plugins/hello-world/src/plugin.test.ts @@ -2,6 +2,6 @@ import plugin from './plugin'; describe('plugin', () => { it('should export plugin', () => { - expect(plugin.id).toBe('hello-world'); + expect(plugin).toBeDefined(); }); }); diff --git a/frontend/packages/plugins/home-page/src/plugin.test.ts b/frontend/packages/plugins/home-page/src/plugin.test.ts index 12dbe91d5a..ed32330478 100644 --- a/frontend/packages/plugins/home-page/src/plugin.test.ts +++ b/frontend/packages/plugins/home-page/src/plugin.test.ts @@ -2,6 +2,6 @@ import plugin from './plugin'; describe('home-page', () => { it('should export plugin', () => { - expect(plugin.id).toBe('home-page'); + expect(plugin).toBeDefined(); }); }); diff --git a/frontend/packages/plugins/login/src/plugin.test.ts b/frontend/packages/plugins/login/src/plugin.test.ts index f7115384da..eea19442ce 100644 --- a/frontend/packages/plugins/login/src/plugin.test.ts +++ b/frontend/packages/plugins/login/src/plugin.test.ts @@ -2,6 +2,6 @@ import plugin from './plugin'; describe('login', () => { it('should export plugin', () => { - expect(plugin.id).toBe('login'); + expect(plugin).toBeDefined(); }); });