From 940d472df104568c6b6cf23b2f5e6dbdaf37b33b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 6 Feb 2020 10:00:28 +0100 Subject: [PATCH] frontend/core: simplify plugin output handling --- frontend/packages/app/src/entities/index.ts | 8 +- frontend/packages/core/src/api/api.ts | 6 +- .../packages/core/src/api/app/AppBuilder.tsx | 31 ++++- ...wPageBuilder.tsx => EntityPageBuilder.tsx} | 86 ++++++++------ .../packages/core/src/api/plugin/Plugin.tsx | 106 +++++++++--------- .../core/src/api/plugin/PluginOutputHook.ts | 11 -- .../packages/core/src/api/plugin/outputs.ts | 8 -- .../packages/core/src/api/plugin/types.ts | 41 +++++++ .../src/components/EntityLink/EntityLink.tsx | 2 +- .../src/components/BuildPage/BuildPage.tsx | 22 ---- .../src/components/BuildPage/index.ts | 1 - .../plugins/github-actions/src/plugin.ts | 14 +-- 12 files changed, 187 insertions(+), 149 deletions(-) rename frontend/packages/core/src/api/entityView/{EntityViewPageBuilder.tsx => EntityPageBuilder.tsx} (52%) delete mode 100644 frontend/packages/core/src/api/plugin/PluginOutputHook.ts delete mode 100644 frontend/packages/core/src/api/plugin/outputs.ts create mode 100644 frontend/packages/core/src/api/plugin/types.ts delete mode 100644 frontend/packages/plugins/github-actions/src/components/BuildPage/BuildPage.tsx delete mode 100644 frontend/packages/plugins/github-actions/src/components/BuildPage/index.ts diff --git a/frontend/packages/app/src/entities/index.ts b/frontend/packages/app/src/entities/index.ts index 8b42342e5d..38e529d87f 100644 --- a/frontend/packages/app/src/entities/index.ts +++ b/frontend/packages/app/src/entities/index.ts @@ -1,7 +1,7 @@ import { createEntityKind, createWidgetView, - createEntityView, + createEntityPage, } from '@backstage/core'; import ComputerIcon from '@material-ui/icons/Computer'; import MockEntityPage from './MockEntityPage'; @@ -13,10 +13,10 @@ const serviceOverviewPage = createWidgetView() .addComponent(MockEntityCard) .addComponent(MockEntityCard); -const serviceView = createEntityView() - .addPage('Overview', 'overview', serviceOverviewPage) +const serviceView = createEntityPage() + .addPage('Overview', '/overview', serviceOverviewPage) .register(GithubActionsPlugin) - .addComponent('Deployment', 'deployment', MockEntityPage); + .addComponent('Deployment', '/deployment', MockEntityPage); const serviceEntity = createEntityKind({ kind: 'service', diff --git a/frontend/packages/core/src/api/api.ts b/frontend/packages/core/src/api/api.ts index 73ca5c1da3..9bff1e2a46 100644 --- a/frontend/packages/core/src/api/api.ts +++ b/frontend/packages/core/src/api/api.ts @@ -1,7 +1,7 @@ import AppBuilder from './app/AppBuilder'; import EntityKind, { EntityConfig } from './entity/EntityKind'; import WidgetViewBuilder from './widgetView/WidgetViewBuilder'; -import EntityViewBuilder from './entityView/EntityViewPageBuilder'; +import EntityPageBuilder from './entityView/EntityPageBuilder'; import BackstagePlugin, { PluginConfig } from './plugin/Plugin'; export function createApp() { @@ -16,8 +16,8 @@ export function createWidgetView() { return new WidgetViewBuilder(); } -export function createEntityView() { - return new EntityViewBuilder(); +export function createEntityPage() { + return new EntityPageBuilder(); } export function createPlugin(config: PluginConfig): BackstagePlugin { diff --git a/frontend/packages/core/src/api/app/AppBuilder.tsx b/frontend/packages/core/src/api/app/AppBuilder.tsx index 349d9a7b06..1d2f9deb4d 100644 --- a/frontend/packages/core/src/api/app/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 { Route, Switch, useParams, Redirect } from 'react-router-dom'; import { AppContextProvider } from './AppContext'; import { App, AppComponentBuilder } from './types'; import EntityKind, { EntityConfig } from '../entity/EntityKind'; import { EntityContextProvider } from '../entityView/EntityContext'; -import BackstagePlugin, { registerSymbol } from '../plugin/Plugin'; +import BackstagePlugin from '../plugin/Plugin'; class AppImpl implements App { constructor(private readonly entities: Map) {} @@ -97,8 +97,31 @@ export default class AppBuilder { const pluginRoutes = new Array(); for (const plugin of this.plugins.values()) { - const { routes = [] } = plugin[registerSymbol](); - pluginRoutes.push(...routes); + for (const output of plugin.output()) { + switch (output.type) { + case 'route': { + const { path, component, options = {} } = output; + const { exact = true } = options; + pluginRoutes.push( + , + ); + break; + } + case 'redirect-route': { + const { path, target, options = {} } = output; + const { exact = true } = options; + pluginRoutes.push( + , + ); + break; + } + } + } } const routes = [...pluginRoutes, ...entityRoutes]; diff --git a/frontend/packages/core/src/api/entityView/EntityViewPageBuilder.tsx b/frontend/packages/core/src/api/entityView/EntityPageBuilder.tsx similarity index 52% rename from frontend/packages/core/src/api/entityView/EntityViewPageBuilder.tsx rename to frontend/packages/core/src/api/entityView/EntityPageBuilder.tsx index e4089e16d7..8dbf6ba2a0 100644 --- a/frontend/packages/core/src/api/entityView/EntityViewPageBuilder.tsx +++ b/frontend/packages/core/src/api/entityView/EntityPageBuilder.tsx @@ -5,8 +5,7 @@ import ListItem from '@material-ui/core/ListItem'; import { AppComponentBuilder, App } from '../app/types'; import { useEntity, useEntityUri, useEntityConfig } from './EntityContext'; import EntityLink from '../../components/EntityLink/EntityLink'; -import BackstagePlugin, { outputSymbol } from '../plugin/Plugin'; -import { entityViewPage } from '../plugin/outputs'; +import BackstagePlugin from '../plugin/Plugin'; const EntityLayout: FC<{}> = ({ children }) => { const config = useEntityConfig(); @@ -34,43 +33,48 @@ const EntitySidebarItem: FC<{ title: string; path: string }> = ({ ); }; -type EntityViewPage = { +type EntityPageNavItem = { title: string; + target: string; +}; + +type EntityPageView = { path: string; component: ComponentType; }; type Props = { - pages: EntityViewPage[]; + navItems: EntityPageNavItem[]; + views: EntityPageView[]; }; -const EntityViewComponent: FC = ({ pages }) => { +const EntityPageComponent: FC = ({ navItems, views }) => { const { kind, id } = useEntity(); const basePath = `/entity/${kind}/${id}`; return ( - {pages.map(({ title, path }) => ( - + {navItems.map(({ title, target }) => ( + ))} - {pages.map(({ path, component }) => ( + {views.map(({ path, component }) => ( ))} - + ); }; -type EntityViewRegistration = +type EntityPageRegistration = | { type: 'page'; title: string; @@ -88,14 +92,14 @@ type EntityViewRegistration = component: ComponentType; }; -export default class EntityViewBuilder extends AppComponentBuilder { - private readonly registrations = new Array(); +export default class EntityPageBuilder extends AppComponentBuilder { + private readonly registrations = new Array(); addPage( title: string, path: string, page: AppComponentBuilder, - ): EntityViewBuilder { + ): EntityPageBuilder { this.registrations.push({ type: 'page', title, path, page }); return this; } @@ -104,42 +108,60 @@ export default class EntityViewBuilder extends AppComponentBuilder { title: string, path: string, component: ComponentType, - ): EntityViewBuilder { + ): EntityPageBuilder { this.registrations.push({ type: 'component', title, path, component }); return this; } - register(plugin: BackstagePlugin): EntityViewBuilder { + register(plugin: BackstagePlugin): EntityPageBuilder { this.registrations.push({ type: 'plugin', plugin }); return this; } build(app: App): ComponentType { - const pages = this.registrations.map(registration => { - switch (registration.type) { + const navItems = new Array(); + const views = new Array(); + + for (const reg of this.registrations) { + switch (reg.type) { case 'page': { - const { title, path, page } = registration; - return { title, path, component: page.build(app) }; + const { title, path, page } = reg; + navItems.push({ title, target: path }); + views.push({ path, component: page.build(app) }); + break; } case 'component': { - const { title, path, component } = registration; - return { title, path, component }; + const { title, path, component } = reg; + navItems.push({ title, target: path }); + views.push({ path, component }); + break; } case 'plugin': { - const { plugin } = registration; - const output = plugin[outputSymbol](entityViewPage); - if (!output) { + let added = false; + for (const output of reg.plugin.output()) { + switch (output.type) { + case 'entity-page-nav-item': + const { title, target } = output; + navItems.push({ title, target }); + added = true; + break; + case 'entity-page-view-route': + const { path, component } = output; + views.push({ path, component }); + added = true; + break; + } + } + if (!added) { throw new Error( - `Plugin ${plugin} was registered as entity view, but did not have any output`, + `Plugin ${reg.plugin} was registered as entity view, but did not provide any output`, ); } - return output; + break; } - default: - throw new Error(`Unknown EntityViewBuilder registration`); } - }); + } - return () => ; + return () => ; } } diff --git a/frontend/packages/core/src/api/plugin/Plugin.tsx b/frontend/packages/core/src/api/plugin/Plugin.tsx index a5714fb009..e351801047 100644 --- a/frontend/packages/core/src/api/plugin/Plugin.tsx +++ b/frontend/packages/core/src/api/plugin/Plugin.tsx @@ -1,6 +1,5 @@ -import React from 'react'; -import { Route, Redirect } from 'react-router-dom'; -import PluginOutputHook from './PluginOutputHook'; +import { ComponentType } from 'react'; +import { PluginOutput, RoutePath, RouteOptions } from './types'; export type PluginConfig = { id: string; @@ -8,101 +7,98 @@ export type PluginConfig = { }; export type PluginHooks = { - router: Router; - provide(ref: PluginOutputHook, value: T): void; + router: RouterHooks; + entityPage: EntityPageHooks; }; -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 = { +export type RouterHooks = { registerRoute( - path: string, - Component: React.ComponentType, + path: RoutePath, + Component: ComponentType, options?: RouteOptions, ): void; + registerRedirect( - path: string, - target: string, - options?: RedirectOptions, + path: RoutePath, + target: RoutePath, + options?: RouteOptions, ): void; }; -export type PluginRegistrationResult = { - routes?: JSX.Element[]; - outputs?: Map, any>; +type EntityPageSidebarItemOptions = { + title: string; + target: RoutePath; +}; + +export type EntityPageHooks = { + navItem(options: EntityPageSidebarItemOptions): void; + route( + path: RoutePath, + component: ComponentType, + options?: RouteOptions, + ): void; }; export const registerSymbol = Symbol('plugin-register'); export const outputSymbol = Symbol('plugin-output'); export default class Plugin { - private result?: PluginRegistrationResult; + private storedOutput?: PluginOutput[]; constructor(private readonly config: PluginConfig) {} - [registerSymbol](): PluginRegistrationResult { - if (this.result) { - return this.result; + output(): PluginOutput[] { + if (this.storedOutput) { + return this.storedOutput; } if (!this.config.register) { - return {}; + return []; } const { id } = this.config; - const routes = new Array(); - const outputs = new Map, any>(); + const outputs = new Array(); this.config.register({ router: { - registerRoute(path, component, options = {}) { + 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( - , - ); + outputs.push({ type: 'route', path, component, options }); }, - registerRedirect(path, target, options = {}) { + 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( - , - ); + outputs.push({ type: 'redirect-route', path, target, options }); }, }, - provide(hook, value) { - outputs.set(hook, value); + entityPage: { + navItem({ title, target }) { + outputs.push({ + type: 'entity-page-nav-item', + target, + title, + }); + }, + route(path, component, options) { + outputs.push({ + type: 'entity-page-view-route', + path, + component, + options, + }); + }, }, }); - this.result = { routes, outputs }; - return this.result; - } - - [outputSymbol](outputHook: PluginOutputHook): T | undefined { - const { outputs } = this[registerSymbol](); - return outputs?.get(outputHook) as T; + this.storedOutput = outputs; + return this.storedOutput; } toString() { diff --git a/frontend/packages/core/src/api/plugin/PluginOutputHook.ts b/frontend/packages/core/src/api/plugin/PluginOutputHook.ts deleted file mode 100644 index 2780117d24..0000000000 --- a/frontend/packages/core/src/api/plugin/PluginOutputHook.ts +++ /dev/null @@ -1,11 +0,0 @@ -export default class PluginOutputHook { - constructor(private readonly name: string) {} - - get T(): T { - throw new Error('use typeof instead'); - } - - toString() { - return `pluginOutput{${this.name}}`; - } -} diff --git a/frontend/packages/core/src/api/plugin/outputs.ts b/frontend/packages/core/src/api/plugin/outputs.ts deleted file mode 100644 index 59c4555cbf..0000000000 --- a/frontend/packages/core/src/api/plugin/outputs.ts +++ /dev/null @@ -1,8 +0,0 @@ -import PluginOutputHook from './PluginOutputHook'; -import { ComponentType } from 'react'; - -export const entityViewPage = new PluginOutputHook<{ - title: string; - path: string; - component: ComponentType; -}>('entity-view-page'); diff --git a/frontend/packages/core/src/api/plugin/types.ts b/frontend/packages/core/src/api/plugin/types.ts new file mode 100644 index 0000000000..ddc6364ec5 --- /dev/null +++ b/frontend/packages/core/src/api/plugin/types.ts @@ -0,0 +1,41 @@ +import { ComponentType } from 'react'; + +export type RouteOptions = { + // Whether the route path must match exactly, defaults to true. + exact?: boolean; +}; + +export type RoutePath = string; + +export type RouteOutput = { + type: 'route'; + path: RoutePath; + component: ComponentType<{}>; + options?: RouteOptions; +}; + +export type RedirectRouteOutput = { + type: 'redirect-route'; + path: RoutePath; + target: RoutePath; + options?: RouteOptions; +}; + +export type EntityPageViewRouteOutput = { + type: 'entity-page-view-route'; + path: RoutePath; + component: ComponentType; + options?: RouteOptions; +}; + +export type EntityPageNavItemOutput = { + type: 'entity-page-nav-item'; + title: string; + target: RoutePath; +}; + +export type PluginOutput = + | RouteOutput + | RedirectRouteOutput + | EntityPageViewRouteOutput + | EntityPageNavItemOutput; diff --git a/frontend/packages/core/src/components/EntityLink/EntityLink.tsx b/frontend/packages/core/src/components/EntityLink/EntityLink.tsx index a060cb1719..04b070e397 100644 --- a/frontend/packages/core/src/components/EntityLink/EntityLink.tsx +++ b/frontend/packages/core/src/components/EntityLink/EntityLink.tsx @@ -16,7 +16,7 @@ type Props = { export function buildPath(kind: string, id?: string, subPath?: string) { if (id) { if (subPath) { - return `/entity/${kind}/${id}/${subPath}`; + return `/entity/${kind}/${id}/${subPath.replace(/^\//, '')}`; } return `/entity/${kind}/${id}`; } diff --git a/frontend/packages/plugins/github-actions/src/components/BuildPage/BuildPage.tsx b/frontend/packages/plugins/github-actions/src/components/BuildPage/BuildPage.tsx deleted file mode 100644 index 6fff20640a..0000000000 --- a/frontend/packages/plugins/github-actions/src/components/BuildPage/BuildPage.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React, { FC } from 'react'; -import { Switch, Route } from 'react-router-dom'; -import BuildListPage from '../BuildListPage'; -import BuildDetailsPage from '../BuildDetailsPage'; -import { useRouteMatch } from 'react-router-dom'; - -const BuildPage: FC<{}> = () => { - const match = useRouteMatch(); - return ( - - ( - - )} - /> - - - ); -}; - -export default BuildPage; diff --git a/frontend/packages/plugins/github-actions/src/components/BuildPage/index.ts b/frontend/packages/plugins/github-actions/src/components/BuildPage/index.ts deleted file mode 100644 index 092d2e639d..0000000000 --- a/frontend/packages/plugins/github-actions/src/components/BuildPage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './BuildPage'; diff --git a/frontend/packages/plugins/github-actions/src/plugin.ts b/frontend/packages/plugins/github-actions/src/plugin.ts index 359ba2e7f9..cecf142ca6 100644 --- a/frontend/packages/plugins/github-actions/src/plugin.ts +++ b/frontend/packages/plugins/github-actions/src/plugin.ts @@ -1,6 +1,6 @@ import { createPlugin } from '@backstage/core'; -import { entityViewPage } from '@backstage/core/src/api/plugin/outputs'; -import BuildPage from './components/BuildPage'; +import BuildDetailsPage from './components/BuildDetailsPage'; +import BuildListPage from './components/BuildListPage'; // export const buildListRoute = createEntityRoute<[]>('/builds') // export const buildDetailsRoute = createEntityRoute<[number]>('/builds/:buildId') @@ -8,11 +8,9 @@ import BuildPage from './components/BuildPage'; export default createPlugin({ id: 'github-actions', - register({ provide }) { - provide(entityViewPage, { - title: 'CI/CD', - path: 'builds', - component: BuildPage, - }); + register({ entityPage }) { + entityPage.navItem({ title: 'CI/CD', target: '/builds' }); + entityPage.route('/builds', BuildListPage); + entityPage.route('/builds/:buildId', BuildDetailsPage); }, });