frontend/core: added plugin route registration api + moved home page to plugin

This commit is contained in:
Patrik Oldsberg
2020-02-05 15:10:01 +01:00
parent 6ac93a293b
commit bda7cb8b7a
23 changed files with 182 additions and 60 deletions
+1
View File
@@ -5,6 +5,7 @@
"dependencies": {
"@backstage/core": "0.0.0",
"@backstage/plugin-hello-world": "0.0.0",
"@backstage/plugin-home-page": "0.0.0",
"@backstage/plugin-login": "0.0.0",
"@react-workspaces/react-scripts": "^3.3.0-alpha-08",
"@testing-library/jest-dom": "^4.2.4",
-1
View File
@@ -6,6 +6,5 @@ describe('App', () => {
it('renders learn react link', () => {
const rendered = render(<App />);
rendered.getByText('This is Backstage!');
rendered.getByText('…with plugin hello-world:');
});
});
+3 -25
View File
@@ -1,17 +1,15 @@
import {
BackstageTheme,
createApp,
EntityLink,
Header,
InfoCard,
Page,
theme,
} from '@backstage/core';
import helloWorld, { MyComponent } from '@backstage/plugin-hello-world';
//import PageHeader from './components/PageHeader';
import { LoginComponent } from '@backstage/plugin-login';
import HomePagePlugin from '@backstage/plugin-home-page';
import { CssBaseline, makeStyles, ThemeProvider } from '@material-ui/core';
import Typography from '@material-ui/core/Typography';
import React, { FC } from 'react';
import {
BrowserRouter as Router,
@@ -58,27 +56,6 @@ const useStyles = makeStyles(theme => ({
},
}));
const Home: FC<{}> = () => {
return (
<InfoCard title="Home Page">
<Typography variant="body1">
{' '}
with plugin {helloWorld?.id ?? 'wat'}:
</Typography>
<MyComponent />
<div>
<RouterLink to="/login">Go to Login</RouterLink>
<EntityLink kind="service" id="backstage-backend">
Backstage Backend
</EntityLink>
<EntityLink uri="entity:service:backstage-lb" subPath="ci-cd">
Backstage LB CI/CD
</EntityLink>
</div>
</InfoCard>
);
};
const Login: FC<{}> = () => {
return (
<InfoCard title="Login Page">
@@ -111,7 +88,8 @@ const AppShell: FC<{}> = ({ children }) => {
const app = createApp();
app.registerEntityKind(...entities);
app.setHomePage(Home);
app.registerPlugin(HomePagePlugin);
const AppComponent = app.build();
const App: FC<{}> = () => {
+1 -1
View File
@@ -15,7 +15,7 @@
"@types/react-router-dom": "^5.1.3",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-helmet":"5.2.1",
"react-helmet": "5.2.1",
"react-addons-text-content": "0.0.4",
"react-router-dom": "^5.1.2",
"recompose": "0.30.0"
@@ -1,13 +1,10 @@
import React, { ComponentType, FC } from 'react';
import { AppContextProvider } from './AppContext';
import { App, EntityConfig, AppComponentBuilder } from './types';
import { Route, Switch, useParams } from 'react-router-dom';
import { Route, Switch, useParams, Redirect } from 'react-router-dom';
import EntityKind from './EntityKind';
import { EntityContextProvider } from './EntityContext';
const DefaultHomePage: FC<{}> = () => {
return <span>Hello! I am default home page</span>;
};
import { BackstagePlugin } from './types';
class AppImpl implements App {
constructor(private readonly entities: Map<string, EntityKind>) {}
@@ -33,7 +30,7 @@ function builtComponent(
export default class AppBuilder {
private readonly entities = new Map<string, EntityKind>();
private homePage: ComponentType = DefaultHomePage;
private readonly plugins = new Map<string, BackstagePlugin>();
registerEntityKind(...entity: EntityKind[]) {
for (const e of entity) {
@@ -45,14 +42,20 @@ export default class AppBuilder {
}
}
setHomePage(page: ComponentType<{}>) {
this.homePage = page;
registerPlugin(...plugin: BackstagePlugin[]) {
for (const p of plugin) {
const { id } = p;
if (this.plugins.has(id)) {
throw new Error(`Plugin '${id}' is already registered`);
}
this.plugins.set(id, p);
}
}
build(): ComponentType<{}> {
const app = new AppImpl(this.entities);
const entityRoutes = [];
const entityRoutes = new Array<JSX.Element>();
for (const { config } of this.entities.values()) {
const { kind, pages } = config;
@@ -92,13 +95,41 @@ export default class AppBuilder {
}
}
const routes = [...entityRoutes];
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 = [...pluginRoutes, ...entityRoutes];
return () => (
<AppContextProvider app={app}>
<Switch>
{routes}
<Route exact path="/" component={this.homePage} />
<Route component={() => <span>404 Not Found</span>} />
</Switch>
</AppContextProvider>
+5 -1
View File
@@ -1,5 +1,5 @@
import AppBuilder from './AppBuilder';
import { EntityConfig } from './types';
import { EntityConfig, PluginConfig, BackstagePlugin } from './types';
import EntityKind from './EntityKind';
import OverviewPageBuilder from './OverviewPageBuilder';
import EntityViewBuilder from './EntityViewPageBuilder';
@@ -19,3 +19,7 @@ export function createOverviewPage() {
export function createEntityView() {
return new EntityViewBuilder();
}
export function createPlugin(config: PluginConfig): BackstagePlugin {
return { register() {}, ...config };
}
@@ -0,0 +1,10 @@
import { BackstagePlugin, PluginConfig } from './types';
function createPlugin(config: PluginConfig): BackstagePlugin {
return {
register() {},
...config,
};
}
export default createPlugin;
@@ -34,3 +34,40 @@ export type UserApi = {
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;
};
@@ -1,11 +0,0 @@
import { BackstagePlugin } from './types';
export type PluginConfig = {
id: string;
};
function createPlugin(config: PluginConfig): BackstagePlugin {
return config;
}
export default createPlugin;
-2
View File
@@ -1,6 +1,4 @@
export * from './appApi';
export * from './types';
export { default as createPlugin } from './createPlugin';
export { default as Page } from '../src/layout/Page';
export { gradients, theme } from '../src/layout/Page';
export { default as Header } from '../src/layout/Header/Header';
-3
View File
@@ -1,3 +0,0 @@
export type BackstagePlugin = {
id: string;
};
@@ -0,0 +1 @@
Welcome to your home-page plugin!
@@ -0,0 +1,4 @@
module.exports = {
...require('@spotify/web-scripts/config/jest.config.js'),
setupFilesAfterEnv: ['../jest.setup.ts'],
};
@@ -0,0 +1 @@
import '@testing-library/jest-dom/extend-expect';
@@ -0,0 +1,26 @@
{
"name": "@backstage/plugin-home-page",
"version": "0.0.0",
"main": "src/index.ts",
"main:src": "src/index.ts",
"devDependencies": {
"@backstage/core": "0.0.0",
"@spotify/web-scripts": "^6.0.0",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/jest": "^24.0.0",
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1"
},
"scripts": {
"lint": "web-scripts lint",
"test": "web-scripts test"
},
"license": "Apache-2.0"
}
@@ -0,0 +1,10 @@
import React from 'react';
import { render } from '@testing-library/react';
import HomePage from './HomePage';
describe('HomePage', () => {
it('should render', () => {
const rendered = render(<HomePage />);
expect(rendered.baseElement).toBeInTheDocument();
});
});
@@ -0,0 +1,23 @@
import React, { FC } from 'react';
import { InfoCard, EntityLink } from '@backstage/core';
import { Link } from 'react-router-dom';
import { Typography } from '@material-ui/core';
const HomePage: FC<{}> = () => {
return (
<InfoCard title="Home Page">
<Typography variant="body1">Welcome to Backstage!</Typography>
<div>
<Link to="/login">Go to Login</Link>
<EntityLink kind="service" id="backstage-backend">
Backstage Backend
</EntityLink>
<EntityLink uri="entity:service:backstage-lb" subPath="ci-cd">
Backstage LB CI/CD
</EntityLink>
</div>
</InfoCard>
);
};
export default HomePage;
@@ -0,0 +1 @@
export { default } from './HomePage';
@@ -0,0 +1 @@
export { default } from './plugin';
@@ -0,0 +1,7 @@
import plugin from './plugin';
describe('home-page', () => {
it('should export plugin', () => {
expect(plugin.id).toBe('home-page');
});
});
@@ -0,0 +1,9 @@
import { createPlugin } from '@backstage/core';
import HomePage from './components/HomePage';
export default createPlugin({
id: 'home-page',
register({ router }) {
router.registerRoute('/', HomePage);
},
});
-5
View File
@@ -15388,11 +15388,6 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.2:
resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tinycolor2@1.4.1:
version "1.4.1"
resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8"
integrity sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"