frontend/core: restructure api a bit

This commit is contained in:
Patrik Oldsberg
2020-02-05 16:44:33 +01:00
parent 9440212e96
commit 2485df1495
25 changed files with 205 additions and 180 deletions
+2 -2
View File
@@ -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);
+25
View File
@@ -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);
}
@@ -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<string, EntityKind>) {}
@@ -30,7 +30,7 @@ function builtComponent(
export default class AppBuilder {
private readonly entities = new Map<string, EntityKind>();
private readonly plugins = new Map<string, BackstagePlugin>();
private readonly plugins = new Set<BackstagePlugin>();
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<JSX.Element>();
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(
<Route path={path} component={component} {...options} />,
);
},
registerRedirect(path, target, options = {}) {
if (path.startsWith('/entity/')) {
throw new Error(
`Plugin ${plugin.id} tried to register forbidden redirect ${path}`,
);
}
pluginRoutes.push(
<Redirect path={path} to={target} {...options} />,
);
},
},
});
const { routes = [] } = plugin[registerSymbol]();
pluginRoutes.push(...routes);
}
const routes = [...pluginRoutes, ...entityRoutes];
@@ -0,0 +1,12 @@
import { ComponentType } from 'react';
import { EntityConfig } from '../entity/EntityKind';
export type App = {
getEntityConfig(kind: string): EntityConfig;
};
export class AppComponentBuilder<T = any> {
build(_app: App): ComponentType<T> {
throw new Error('Must override build() in AppComponentBuilder');
}
}
@@ -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) {}
}
@@ -1,5 +1,5 @@
import React, { createContext, useContext, FC } from 'react';
import { EntityConfig } from './types';
import { EntityConfig } from '../entity/EntityKind';
type Value = {
config: EntityConfig;
@@ -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();
+8
View File
@@ -0,0 +1,8 @@
export * from './types';
export * from './api';
export { useApp } from './app/AppContext';
export {
useEntity,
useEntityConfig,
useEntityUri,
} from './entityView/EntityContext';
@@ -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<any>,
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<JSX.Element>();
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(
<Route
key={path}
path={path}
component={component}
exact={exact}
/>,
);
},
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(
<Redirect key={path} path={path} to={target} exact={exact} />,
);
},
},
});
this.result = { routes };
return this.result;
}
toString() {
return `plugin{${this.config.id}}`;
}
}
+10
View File
@@ -0,0 +1,10 @@
export type User = {
id: string;
email: string;
};
export type UserApi = {
isLoggedIn(): Promise<boolean>;
getUser(): Promise<User>;
};
@@ -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<any>[];
};
const OverviewPageComponent: FC<Props> = ({ cards }) => {
const WidgetViewComponent: FC<Props> = ({ cards }) => {
return (
<div>
{cards.map(CardComponent => (
@@ -16,16 +16,16 @@ const OverviewPageComponent: FC<Props> = ({ cards }) => {
);
};
type OverviewPageRegistration = {
type WidgetViewRegistration = {
type: 'component';
component: ComponentType<any>;
};
export default class OverviewPageBuilder extends AppComponentBuilder {
private readonly registrations = new Array<OverviewPageRegistration>();
export default class WidgetViewBuilder extends AppComponentBuilder {
private readonly registrations = new Array<WidgetViewRegistration>();
private output?: ComponentType<any>;
addComponent(component: ComponentType<any>): OverviewPageBuilder {
addComponent(component: ComponentType<any>): 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 = () => <OverviewPageComponent app={app} cards={cards} />;
this.output = () => <WidgetViewComponent app={app} cards={cards} />;
return this.output;
}
}
@@ -1,5 +0,0 @@
import { EntityConfig } from './types';
export default class EntityKind {
constructor(readonly config: EntityConfig) {}
}
-25
View File
@@ -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 };
}
@@ -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');
});
});
@@ -1,10 +0,0 @@
import { BackstagePlugin, PluginConfig } from './types';
function createPlugin(config: PluginConfig): BackstagePlugin {
return {
register() {},
...config,
};
}
export default createPlugin;
@@ -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';
@@ -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<T = any> {
build(_app: App): ComponentType<T> {
throw new Error('Must override build() in AppComponentBuilder');
}
}
export type User = {
id: string;
email: string;
};
export type UserApi = {
isLoggedIn(): Promise<boolean>;
getUser(): Promise<User>;
};
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<any>,
options?: RouteOptions,
): void;
registerRedirect(
path: string,
target: string,
options?: RedirectOptions,
): void;
};
@@ -0,0 +1 @@
export { default } from './EntityLink';
+2 -2
View File
@@ -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';
@@ -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();
});
});
@@ -2,6 +2,6 @@ import plugin from './plugin';
describe('plugin', () => {
it('should export plugin', () => {
expect(plugin.id).toBe('hello-world');
expect(plugin).toBeDefined();
});
});
@@ -2,6 +2,6 @@ import plugin from './plugin';
describe('home-page', () => {
it('should export plugin', () => {
expect(plugin.id).toBe('home-page');
expect(plugin).toBeDefined();
});
});
@@ -2,6 +2,6 @@ import plugin from './plugin';
describe('login', () => {
it('should export plugin', () => {
expect(plugin.id).toBe('login');
expect(plugin).toBeDefined();
});
});