Merge pull request #53 from spotify/rugvip/core-api-structure
frontend/core: restructure api a bit + fixes
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
+11
-34
@@ -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
-1
@@ -1,5 +1,5 @@
|
||||
import React, { createContext, useContext, FC } from 'react';
|
||||
import { EntityConfig } from './types';
|
||||
import { EntityConfig } from '../entity/EntityKind';
|
||||
|
||||
type Value = {
|
||||
config: EntityConfig;
|
||||
+3
-3
@@ -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();
|
||||
@@ -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}}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export type User = {
|
||||
id: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
export type UserApi = {
|
||||
isLoggedIn(): Promise<boolean>;
|
||||
|
||||
getUser(): Promise<User>;
|
||||
};
|
||||
+8
-8
@@ -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) {}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
+1
-1
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user