frontend: added github-actions plugin skeleton + plugin output experiments

This commit is contained in:
Patrik Oldsberg
2020-02-06 09:01:55 +01:00
parent a4b5117a27
commit b894a0be0c
26 changed files with 243 additions and 7 deletions
+1
View File
@@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"@backstage/core": "0.0.0",
"@backstage/plugin-github-actions": "0.0.0",
"@backstage/plugin-hello-world": "0.0.0",
"@backstage/plugin-home-page": "0.0.0",
"@backstage/plugin-login": "0.0.0",
+3 -1
View File
@@ -6,6 +6,7 @@ import {
import ComputerIcon from '@material-ui/icons/Computer';
import MockEntityPage from './MockEntityPage';
import MockEntityCard from './MockEntityCard';
import GithubActionsPlugin from '@backstage/plugin-github-actions';
/* SERVICE */
const serviceOverviewPage = createWidgetView()
@@ -14,7 +15,8 @@ const serviceOverviewPage = createWidgetView()
const serviceView = createEntityView()
.addPage('Overview', 'overview', serviceOverviewPage)
.addComponent('CI/CD', 'ci-cd', MockEntityPage);
.register(GithubActionsPlugin)
.addComponent('Deployment', 'deployment', MockEntityPage);
const serviceEntity = createEntityKind({
kind: 'service',
@@ -5,6 +5,8 @@ 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';
const EntityLayout: FC<{}> = ({ children }) => {
const config = useEntityConfig();
@@ -57,7 +59,7 @@ const EntityViewComponent: FC<Props> = ({ pages }) => {
{pages.map(({ path, component }) => (
<Route
key={path}
exact
exact={false}
path={`${basePath}/${path}`}
component={component}
/>
@@ -75,6 +77,10 @@ type EntityViewRegistration =
path: string;
page: AppComponentBuilder;
}
| {
type: 'plugin';
plugin: BackstagePlugin;
}
| {
type: 'component';
title: string;
@@ -103,6 +109,11 @@ export default class EntityViewBuilder extends AppComponentBuilder {
return this;
}
register(plugin: BackstagePlugin): EntityViewBuilder {
this.registrations.push({ type: 'plugin', plugin });
return this;
}
build(app: App): ComponentType<any> {
const pages = this.registrations.map(registration => {
switch (registration.type) {
@@ -114,6 +125,16 @@ export default class EntityViewBuilder extends AppComponentBuilder {
const { title, path, component } = registration;
return { title, path, component };
}
case 'plugin': {
const { plugin } = registration;
const output = plugin[outputSymbol](entityViewPage);
if (!output) {
throw new Error(
`Plugin ${plugin} was registered as entity view, but did not have any output`,
);
}
return output;
}
default:
throw new Error(`Unknown EntityViewBuilder registration`);
}
@@ -1,5 +1,6 @@
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import PluginOutputHook from './PluginOutputHook';
export type PluginConfig = {
id: string;
@@ -8,6 +9,7 @@ export type PluginConfig = {
export type PluginHooks = {
router: Router;
provide<T>(ref: PluginOutputHook<T>, value: T): void;
};
export type RouteOptions = {
@@ -35,9 +37,11 @@ export type Router = {
export type PluginRegistrationResult = {
routes?: JSX.Element[];
outputs?: Map<PluginOutputHook<any>, any>;
};
export const registerSymbol = Symbol('plugin-register');
export const outputSymbol = Symbol('plugin-output');
export default class Plugin {
private result?: PluginRegistrationResult;
@@ -55,6 +59,7 @@ export default class Plugin {
const { id } = this.config;
const routes = new Array<JSX.Element>();
const outputs = new Map<PluginOutputHook<any>, any>();
this.config.register({
router: {
@@ -86,12 +91,20 @@ export default class Plugin {
);
},
},
provide(hook, value) {
outputs.set(hook, value);
},
});
this.result = { routes };
this.result = { routes, outputs };
return this.result;
}
[outputSymbol]<T>(outputHook: PluginOutputHook<T>): T | undefined {
const { outputs } = this[registerSymbol]();
return outputs?.get(outputHook) as T;
}
toString() {
return `plugin{${this.config.id}}`;
}
@@ -0,0 +1,11 @@
export default class PluginOutputHook<T> {
constructor(private readonly name: string) {}
get T(): T {
throw new Error('use typeof instead');
}
toString() {
return `pluginOutput{${this.name}}`;
}
}
@@ -0,0 +1,8 @@
import PluginOutputHook from './PluginOutputHook';
import { ComponentType } from 'react';
export const entityViewPage = new PluginOutputHook<{
title: string;
path: string;
component: ComponentType<any>;
}>('entity-view-page');
@@ -9,8 +9,8 @@ type Props = {
const WidgetViewComponent: FC<Props> = ({ cards }) => {
return (
<div>
{cards.map(CardComponent => (
<CardComponent />
{cards.map((CardComponent, index) => (
<CardComponent key={index} />
))}
</div>
);
@@ -13,7 +13,7 @@ type Props = {
}
);
function buildPath(kind: string, id?: string, subPath?: string) {
export function buildPath(kind: string, id?: string, subPath?: string) {
if (id) {
if (subPath) {
return `/entity/${kind}/${id}/${subPath}`;
@@ -0,0 +1,16 @@
import React, { FC } from 'react';
import { Link } from 'react-router-dom';
import { useEntity } from '../../api';
import { buildPath } from './EntityLink';
type Props = {
view: string;
};
const RelativeEntityLink: FC<Props> = ({ view, children }) => {
const entity = useEntity();
return <Link to={buildPath(entity.kind, entity.id, view)}>{children}</Link>;
};
export default RelativeEntityLink;
@@ -1 +1,2 @@
export { default } from './EntityLink';
export { default as RelativeEntityLink } from './RelativeEntityLink';
+4 -1
View File
@@ -1,5 +1,8 @@
export * from './api';
export { default as EntityLink } from './components/EntityLink';
export {
default as EntityLink,
RelativeEntityLink,
} 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';
@@ -0,0 +1 @@
Welcome to your github-actions 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,28 @@
{
"name": "@backstage/plugin-github-actions",
"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",
"@types/react-router-dom": "^5.1.3",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-router-dom": "^5.1.2",
"@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,20 @@
import React, { FC } from 'react';
import Button from '@material-ui/core/Button';
type Props = {
buildId: string;
};
const BuildDetailsPage: FC<Props> = ({ buildId }) => {
return (
<Button
variant="contained"
color="primary"
onClick={() => window.location.reload()}
>
Hello! Build details for {buildId}
</Button>
);
};
export default BuildDetailsPage;
@@ -0,0 +1 @@
export { default } from './BuildDetailsPage';
@@ -0,0 +1,8 @@
import React, { FC } from 'react';
import { InfoCard } from '@backstage/core';
const BuildInfoCard: FC<{}> = () => {
return <InfoCard>Last build was acb67fa3b5472w</InfoCard>;
};
export default BuildInfoCard;
@@ -0,0 +1 @@
export { default } from './BuildInfoCard';
@@ -0,0 +1,46 @@
import React, { FC } from 'react';
import {
TableContainer,
Table,
TableHead,
TableRow,
TableCell,
Paper,
TableBody,
} from '@material-ui/core';
import { RelativeEntityLink } from '@backstage/core';
const BuildListPage: FC<{}> = () => {
const rows = [
{ message: 'Fixed a Bar', commit: 'fb46ca3dfbd7af5bc43da', id: 165 },
{ message: 'Fixed a Foo', commit: 'd7af5bc43dafb46ca3dfb', id: 164 },
];
return (
<TableContainer component={Paper}>
<Table aria-label="CI/CD builds table">
<TableHead>
<TableRow>
<TableCell>Message</TableCell>
<TableCell>Commit</TableCell>
<TableCell>Build ID</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map(row => (
<TableRow key={row.commit}>
<TableCell>{row.message}</TableCell>
<TableCell>{row.commit}</TableCell>
<TableCell>
<RelativeEntityLink view={`builds/${row.id}`}>
{row.commit}
</RelativeEntityLink>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};
export default BuildListPage;
@@ -0,0 +1 @@
export { default } from './BuildListPage';
@@ -0,0 +1,22 @@
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 (
<Switch>
<Route
path={`${match.path}/:buildId`}
render={({ match }) => (
<BuildDetailsPage buildId={match.params.buildId} />
)}
/>
<Route path={match.path} component={BuildListPage} />
</Switch>
);
};
export default BuildPage;
@@ -0,0 +1 @@
export { default } from './BuildPage';
@@ -0,0 +1 @@
export { default } from './plugin';
@@ -0,0 +1,7 @@
import plugin from './plugin';
describe('github-actions', () => {
it('should export plugin', () => {
expect(plugin).toBeDefined();
});
});
@@ -0,0 +1,18 @@
import { createPlugin } from '@backstage/core';
import { entityViewPage } from '@backstage/core/src/api/plugin/outputs';
import BuildPage from './components/BuildPage';
// export const buildListRoute = createEntityRoute<[]>('/builds')
// export const buildDetailsRoute = createEntityRoute<[number]>('/builds/:buildId')
export default createPlugin({
id: 'github-actions',
register({ provide }) {
provide(entityViewPage, {
title: 'CI/CD',
path: 'builds',
component: BuildPage,
});
},
});