Merge pull request #56 from spotify/rugvip/plugin-output
frontend: added github-actions plugin skeleton + plugin output experiments
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
+20
@@ -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';
|
||||
+8
@@ -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';
|
||||
+46
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user