Merge pull request #89 from spotify/rugvip/builds-card
front: add plugin api for widgets + build info card for services
This commit is contained in:
@@ -17,8 +17,8 @@ import GithubActionsPlugin from '@backstage/plugin-github-actions';
|
||||
|
||||
/* SERVICE */
|
||||
const serviceOverviewPage = createWidgetView()
|
||||
.addComponent(MockEntityCard)
|
||||
.addComponent(MockEntityCard);
|
||||
.add({ size: 4, component: MockEntityCard })
|
||||
.register(GithubActionsPlugin);
|
||||
|
||||
const serviceView = createEntityPage()
|
||||
.addPage('Overview', WebIcon, '/overview', serviceOverviewPage)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ComponentType } from 'react';
|
||||
import { PluginOutput, RoutePath, RouteOptions } from './types';
|
||||
import { IconComponent } from '../types';
|
||||
import { Widget } from '../widgetView/types';
|
||||
|
||||
export type PluginConfig = {
|
||||
id: string;
|
||||
@@ -10,6 +11,7 @@ export type PluginConfig = {
|
||||
export type PluginHooks = {
|
||||
router: RouterHooks;
|
||||
entityPage: EntityPageHooks;
|
||||
widgets: WidgetHooks;
|
||||
};
|
||||
|
||||
export type RouterHooks = {
|
||||
@@ -41,6 +43,10 @@ export type EntityPageHooks = {
|
||||
): void;
|
||||
};
|
||||
|
||||
export type WidgetHooks = {
|
||||
add(widget: Widget): void;
|
||||
};
|
||||
|
||||
export const registerSymbol = Symbol('plugin-register');
|
||||
export const outputSymbol = Symbol('plugin-output');
|
||||
|
||||
@@ -98,6 +104,11 @@ export default class Plugin {
|
||||
});
|
||||
},
|
||||
},
|
||||
widgets: {
|
||||
add(widget: Widget) {
|
||||
outputs.push({ type: 'widget', widget });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.storedOutput = outputs;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ComponentType } from 'react';
|
||||
import { IconComponent } from '../types';
|
||||
import { Widget } from '../widgetView/types';
|
||||
|
||||
export type RouteOptions = {
|
||||
// Whether the route path must match exactly, defaults to true.
|
||||
@@ -22,6 +23,11 @@ export type RedirectRouteOutput = {
|
||||
options?: RouteOptions;
|
||||
};
|
||||
|
||||
export type WidgetOutput = {
|
||||
type: 'widget';
|
||||
widget: Widget;
|
||||
};
|
||||
|
||||
export type EntityPageViewRouteOutput = {
|
||||
type: 'entity-page-view-route';
|
||||
path: RoutePath;
|
||||
@@ -39,5 +45,6 @@ export type EntityPageNavItemOutput = {
|
||||
export type PluginOutput =
|
||||
| RouteOutput
|
||||
| RedirectRouteOutput
|
||||
| WidgetOutput
|
||||
| EntityPageViewRouteOutput
|
||||
| EntityPageNavItemOutput;
|
||||
|
||||
@@ -1,50 +1,65 @@
|
||||
import React, { ComponentType, FC } from 'react';
|
||||
import React, { ComponentType } from 'react';
|
||||
import { App, AppComponentBuilder } from '../app/types';
|
||||
import { Widget } from './types';
|
||||
import BackstagePlugin from '../plugin/Plugin';
|
||||
import DefaultWidgetView from '../../components/DefaultWidgetView';
|
||||
|
||||
type Props = {
|
||||
app: App;
|
||||
cards: ComponentType<any>[];
|
||||
};
|
||||
|
||||
const WidgetViewComponent: FC<Props> = ({ cards }) => {
|
||||
return (
|
||||
<div>
|
||||
{cards.map((CardComponent, index) => (
|
||||
<CardComponent key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type WidgetViewRegistration = {
|
||||
type: 'component';
|
||||
component: ComponentType<any>;
|
||||
};
|
||||
type WidgetViewRegistration =
|
||||
| {
|
||||
type: 'component';
|
||||
widget: Widget;
|
||||
}
|
||||
| {
|
||||
type: 'plugin';
|
||||
plugin: BackstagePlugin;
|
||||
};
|
||||
|
||||
export default class WidgetViewBuilder extends AppComponentBuilder {
|
||||
private readonly registrations = new Array<WidgetViewRegistration>();
|
||||
private output?: ComponentType<any>;
|
||||
|
||||
addComponent(component: ComponentType<any>): WidgetViewBuilder {
|
||||
this.registrations.push({ type: 'component', component });
|
||||
add(widget: Widget): WidgetViewBuilder {
|
||||
this.registrations.push({ type: 'component', widget });
|
||||
return this;
|
||||
}
|
||||
|
||||
build(app: App): ComponentType<any> {
|
||||
register(plugin: BackstagePlugin): WidgetViewBuilder {
|
||||
this.registrations.push({ type: 'plugin', plugin });
|
||||
return this;
|
||||
}
|
||||
|
||||
build(_app: App): ComponentType<any> {
|
||||
if (this.output) {
|
||||
return this.output;
|
||||
}
|
||||
|
||||
const cards = this.registrations.map(reg => {
|
||||
const widgets = new Array<Widget>();
|
||||
|
||||
for (const reg of this.registrations) {
|
||||
switch (reg.type) {
|
||||
case 'component':
|
||||
return reg.component;
|
||||
widgets.push(reg.widget);
|
||||
break;
|
||||
case 'plugin':
|
||||
let added = false;
|
||||
for (const output of reg.plugin.output()) {
|
||||
if (output.type === 'widget') {
|
||||
widgets.push(output.widget);
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
if (!added) {
|
||||
throw new Error(
|
||||
`Plugin ${reg.plugin} was registered as widget provider, but did not provide any widgets`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown WidgetViewBuilder registration`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.output = () => <WidgetViewComponent app={app} cards={cards} />;
|
||||
this.output = () => <DefaultWidgetView widgets={widgets} />;
|
||||
return this.output;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
export type Widget = {
|
||||
size: 4 | 6 | 8 | 12;
|
||||
component: ComponentType<any>;
|
||||
};
|
||||
|
||||
export type WidgetViewProps = {
|
||||
widgets: Widget[];
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Grid, Paper, makeStyles, Theme } from '@material-ui/core';
|
||||
import { WidgetViewProps } from '../../api/widgetView/types';
|
||||
|
||||
const useStyles = makeStyles<Theme>(theme => ({
|
||||
root: {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
widgetWrapper: {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
const WidgetViewComponent: FC<WidgetViewProps> = ({ widgets }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Grid container direction="row" spacing={2}>
|
||||
{widgets.map(({ size, component: WidgetComponent }, index) => (
|
||||
<Grid key={index} item xs={size}>
|
||||
<Paper className={classes.widgetWrapper}>
|
||||
<WidgetComponent />
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WidgetViewComponent;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './DefaultWidgetView';
|
||||
+81
-2
@@ -1,8 +1,87 @@
|
||||
import React, { FC } from 'react';
|
||||
import { InfoCard } from '@backstage/core';
|
||||
import { RelativeEntityLink } from '@backstage/core';
|
||||
import { BuildsClient } from '../../apis/builds';
|
||||
import { useAsync } from 'react-use';
|
||||
import {
|
||||
Typography,
|
||||
Table,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
LinearProgress,
|
||||
makeStyles,
|
||||
Theme,
|
||||
} from '@material-ui/core';
|
||||
|
||||
const client = BuildsClient.create('http://localhost:8080');
|
||||
|
||||
const useStyles = makeStyles<Theme>(theme => ({
|
||||
root: {
|
||||
// height: 400,
|
||||
},
|
||||
title: {
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
const BuildInfoCard: FC<{}> = () => {
|
||||
return <InfoCard>Last build was acb67fa3b5472w</InfoCard>;
|
||||
const classes = useStyles();
|
||||
const status = useAsync(() => client.listBuilds('entity:spotify:backstage'));
|
||||
|
||||
let content: JSX.Element;
|
||||
|
||||
if (status.loading) {
|
||||
content = <LinearProgress />;
|
||||
} else if (status.error) {
|
||||
content = (
|
||||
<Typography variant="h2" color="error">
|
||||
Failed to load builds, {status.error.message}
|
||||
</Typography>
|
||||
);
|
||||
} else {
|
||||
const [build] =
|
||||
status.value?.filter(({ branch }) => branch === 'master') ?? [];
|
||||
|
||||
content = (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Message</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<RelativeEntityLink
|
||||
view={`builds/${encodeURIComponent(build?.uri || '')}`}
|
||||
>
|
||||
<Typography color="primary">{build?.message}</Typography>
|
||||
</RelativeEntityLink>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Commit ID</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{build?.commitId}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Status</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{build?.status}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Typography variant="h2" className={classes.title}>
|
||||
Master Build
|
||||
</Typography>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BuildInfoCard;
|
||||
|
||||
+3
-3
@@ -35,7 +35,7 @@ const useStyles = makeStyles<Theme>(theme => ({
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
title: {
|
||||
paddingBottom: theme.spacing(2),
|
||||
padding: theme.spacing(1, 0, 2, 0),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -49,7 +49,7 @@ const BuildListPage: FC<{}> = () => {
|
||||
content = <LinearProgress />;
|
||||
} else if (status.error) {
|
||||
content = (
|
||||
<Typography variant="h4" color="error">
|
||||
<Typography variant="h2" color="error">
|
||||
Failed to load builds, {status.error.message}
|
||||
</Typography>
|
||||
);
|
||||
@@ -101,7 +101,7 @@ const BuildListPage: FC<{}> = () => {
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Typography variant="h4" className={classes.title}>
|
||||
<Typography variant="h3" className={classes.title}>
|
||||
CI/CD Builds
|
||||
</Typography>
|
||||
{content}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createPlugin } from '@backstage/core';
|
||||
import BuildDetailsPage from './components/BuildDetailsPage';
|
||||
import BuildListPage from './components/BuildListPage';
|
||||
import BuildIcon from '@material-ui/icons/Build';
|
||||
import BuildInfoCard from './components/BuildInfoCard';
|
||||
|
||||
// export const buildListRoute = createEntityRoute<[]>('/builds')
|
||||
// export const buildDetailsRoute = createEntityRoute<[number]>('/builds/:buildId')
|
||||
@@ -9,9 +10,10 @@ import BuildIcon from '@material-ui/icons/Build';
|
||||
export default createPlugin({
|
||||
id: 'github-actions',
|
||||
|
||||
register({ entityPage }) {
|
||||
register({ entityPage, widgets }) {
|
||||
entityPage.navItem({ title: 'CI/CD', icon: BuildIcon, target: '/builds' });
|
||||
entityPage.route('/builds', BuildListPage);
|
||||
entityPage.route('/builds/:buildUri', BuildDetailsPage);
|
||||
widgets.add({ size: 8, component: BuildInfoCard });
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user