Merge pull request #47 from spotify/rugvip/home-page-plugin
frontend/core: added plugin route registration api + moved home page to plugin
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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:');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<{}> = () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user