Merge branch 'master' of github.com:spotify/backstage into soapraj/call-cookiecutter

This commit is contained in:
Raghunandan Balachandran
2020-02-07 14:27:13 +01:00
19 changed files with 351 additions and 105 deletions
+2
View File
@@ -0,0 +1,2 @@
backend: docker-compose up --build
frontend: cd frontend/ && yarn install && yarn start
+2 -5
View File
@@ -1,8 +1,4 @@
import {
BackstageTheme,
createApp,
InfoCard,
} from '@backstage/core';
import { BackstageTheme, createApp, InfoCard } from '@backstage/core';
//import PageHeader from './components/PageHeader';
import { LoginComponent } from '@backstage/plugin-login';
import HomePagePlugin from '@backstage/plugin-home-page';
@@ -24,6 +20,7 @@ const useStyles = makeStyles(theme => ({
body: {
height: '100%',
fontFamily: theme.typography.fontFamily,
'overscroll-behavior-y': 'none',
},
a: {
color: 'inherit',
@@ -152,7 +152,7 @@ const useStyles = makeStyles(theme => ({
position: 'relative',
overflow: 'visible',
width: theme.spacing(7) + 1,
height: '100vh',
height: '100%',
},
drawer: {
display: 'flex',
+2 -2
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
import { ComponentType } from 'react';
export type IconComponent = ComponentType<{
fontSize: 'inherit' | 'default' | 'small' | 'large';
fontSize?: 'inherit' | 'default' | 'small' | 'large';
}>;
@@ -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[];
};
@@ -6,7 +6,7 @@ import { Switch, Route, Redirect } from 'react-router-dom';
import DefaultEntityPageHeader from '../DefaultEntityPageHeader';
import DefaultEntityPageNavbar from '../DefaultEntityPageNavbar';
const useStyles = makeStyles<Theme>(theme => ({
const useStyles = makeStyles<Theme>({
root: {
display: 'grid',
gridTemplateAreas: `
@@ -16,7 +16,6 @@ const useStyles = makeStyles<Theme>(theme => ({
gridTemplateRows: 'auto 1fr',
gridTemplateColumns: 'auto 1fr',
minHeight: '100%',
paddingBottom: theme.spacing(3),
},
header: {
gridArea: 'header',
@@ -27,7 +26,7 @@ const useStyles = makeStyles<Theme>(theme => ({
content: {
gridArea: 'content',
},
}));
});
const DefaultEntityPage: FC<EntityPageProps> = ({ navItems, views }) => {
const classes = useStyles();
@@ -12,7 +12,7 @@ const useStyles = makeStyles<Theme>({
transitionTimingFunction: 'ease-in',
backgroundColor: '#eeeeee',
boxShadow: '0px 0 4px 0px rgba(0,0,0,0.35)',
height: '100vh',
height: '100%',
},
list: {
padding: 0,
@@ -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';
@@ -15,13 +15,23 @@ import {
makeStyles,
ButtonGroup,
Button,
Theme,
} from '@material-ui/core';
import { RelativeEntityLink } from '@backstage/core';
import BuildStatusIndicator from '../BuildStatusIndicator';
const useStyles = makeStyles({
const useStyles = makeStyles<Theme>(theme => ({
root: {
maxWidth: 720,
margin: theme.spacing(2),
},
});
title: {
padding: theme.spacing(1, 0, 2, 0),
},
table: {
padding: theme.spacing(1),
},
}));
type Props = {};
@@ -47,61 +57,73 @@ const BuildDetailsPage: FC<Props> = () => {
const details = status.value;
return (
<TableContainer component={Paper} className={classes.root}>
<Table>
<TableBody>
<TableRow>
<TableCell>
<Typography noWrap>Branch</Typography>
</TableCell>
<TableCell>{details?.build.branch}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Message</Typography>
</TableCell>
<TableCell>{details?.build.message}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Commit ID</Typography>
</TableCell>
<TableCell>{details?.build.commitId}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Status</Typography>
</TableCell>
<TableCell>{details?.build.status}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Author</Typography>
</TableCell>
<TableCell>{details?.author}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Links</Typography>
</TableCell>
<TableCell>
<ButtonGroup
variant="text"
color="primary"
aria-label="text primary button group"
>
<Button>
<Link href={details?.overviewUrl}>GitHub</Link>
</Button>
<Button>
<Link href={details?.logUrl}>Logs</Link>
</Button>
</ButtonGroup>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<div className={classes.root}>
<Typography className={classes.title} variant="h3">
<RelativeEntityLink view="/builds">
<Typography component={'span'} variant="h3" color="primary">
&lt;{' '}
</Typography>
</RelativeEntityLink>
Build Details
</Typography>
<TableContainer component={Paper} className={classes.table}>
<Table>
<TableBody>
<TableRow>
<TableCell>
<Typography noWrap>Branch</Typography>
</TableCell>
<TableCell>{details?.build.branch}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Message</Typography>
</TableCell>
<TableCell>{details?.build.message}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Commit ID</Typography>
</TableCell>
<TableCell>{details?.build.commitId}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Status</Typography>
</TableCell>
<TableCell>
<BuildStatusIndicator status={details?.build.status} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Author</Typography>
</TableCell>
<TableCell>{details?.author}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Links</Typography>
</TableCell>
<TableCell>
<ButtonGroup
variant="text"
color="primary"
aria-label="text primary button group"
>
<Button>
<Link href={details?.overviewUrl}>GitHub</Link>
</Button>
<Button>
<Link href={details?.logUrl}>Logs</Link>
</Button>
</ButtonGroup>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</div>
);
};
@@ -1,8 +1,90 @@
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';
import BuildStatusIndicator from '../BuildStatusIndicator';
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>
<BuildStatusIndicator status={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;
@@ -16,6 +16,7 @@ import {
import { RelativeEntityLink } from '@backstage/core';
import { BuildsClient } from '../../apis/builds';
import { useAsync } from 'react-use';
import BuildStatusIndicator from '../BuildStatusIndicator';
const client = BuildsClient.create('http://localhost:8080');
@@ -35,7 +36,7 @@ const useStyles = makeStyles<Theme>(theme => ({
padding: theme.spacing(2),
},
title: {
paddingBottom: theme.spacing(2),
padding: theme.spacing(1, 0, 2, 0),
},
}));
@@ -49,7 +50,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>
);
@@ -68,8 +69,9 @@ const BuildListPage: FC<{}> = () => {
<TableBody>
{status.value!.map(build => (
<TableRow key={build.uri}>
{/* TODO: make this an indicating blobby thing */}
<TableCell>{build.status}</TableCell>
<TableCell>
<BuildStatusIndicator status={build.status} />
</TableCell>
<TableCell>
<Typography>
<LongText text={build.branch} max={30} />
@@ -101,7 +103,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}
@@ -0,0 +1,63 @@
import React, { FC } from 'react';
import { makeStyles, Theme } from '@material-ui/core';
import { BuildStatus } from '../../apis/builds';
import FailureIcon from '@material-ui/icons/Error';
import SuccessIcon from '@material-ui/icons/CheckCircle';
import ProgressIcon from '@material-ui/icons/Autorenew';
import UnknownIcon from '@material-ui/icons/Help';
import { IconComponent } from '@backstage/core';
type Props = {
status?: BuildStatus;
};
type StatusStyle = {
icon: IconComponent;
color: string;
};
const styles: { [key in BuildStatus]: StatusStyle } = {
[BuildStatus.Null]: {
icon: UnknownIcon,
color: '#f49b20',
},
[BuildStatus.Success]: {
icon: SuccessIcon,
color: '#1db855',
},
[BuildStatus.Failure]: {
icon: FailureIcon,
color: '#CA001B',
},
[BuildStatus.Pending]: {
icon: UnknownIcon,
color: '#5BC0DE',
},
[BuildStatus.Running]: {
icon: ProgressIcon,
color: '#BEBEBE',
},
};
const useStyles = makeStyles<Theme, StatusStyle>({
icon: style => ({
color: style.color,
}),
});
const BuildStatusIndicator: FC<Props> = props => {
const { status } = props;
const style = (status && styles[status]) || styles[BuildStatus.Null];
const classes = useStyles(style);
const IconComponent = style.icon;
return (
<div className={classes.icon}>
<IconComponent />
</div>
);
};
export default BuildStatusIndicator;
@@ -0,0 +1 @@
export { default } from './BuildStatusIndicator';
@@ -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 });
},
});