Merge pull request #72 from spotify/rugvip/builds-view

frontend/plugins/github-actions: real builds list and detail views
This commit is contained in:
Patrik Oldsberg
2020-02-06 17:37:35 +01:00
committed by GitHub
7 changed files with 245 additions and 40 deletions
@@ -5,6 +5,7 @@
"main:src": "src/index.ts",
"devDependencies": {
"@backstage/core": "0.0.0",
"@backstage/protobuf-definitions": "0.0.0",
"@spotify/web-scripts": "^6.0.0",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
@@ -0,0 +1,59 @@
import { buildsV1 } from '@backstage/protobuf-definitions';
import { BuildStatus, Build, BuildDetails } from './types';
const statusTable = {
[buildsV1.BuildStatus.NULL]: BuildStatus.Null,
[buildsV1.BuildStatus.SUCCESS]: BuildStatus.Success,
[buildsV1.BuildStatus.FAILURE]: BuildStatus.Failure,
[buildsV1.BuildStatus.PENDING]: BuildStatus.Pending,
[buildsV1.BuildStatus.RUNNING]: BuildStatus.Running,
};
export default class BuildsClient {
static create(grpcAddress: string): BuildsClient {
return new BuildsClient(new buildsV1.Client(grpcAddress));
}
constructor(private readonly client: buildsV1.Client) {}
async listBuilds(entityUri: string): Promise<Build[]> {
const req = new buildsV1.ListBuildsRequest();
req.setEntityUri(entityUri);
const res = await this.client.listBuilds(req);
return res.getBuildsList().map(this.transformBuild);
}
async getBuild(buildUri: string): Promise<BuildDetails> {
const req = new buildsV1.GetBuildRequest();
req.setBuildUri(buildUri);
const res = await this.client.getBuild(req);
const build = res.getBuild();
if (!build) {
throw new Error('No build in GetBuild response');
}
const details = res.getDetails();
if (!details) {
throw new Error('No details in GetBuild response');
}
return {
build: this.transformBuild(build),
author: details.getAuthor(),
logUrl: details.getLogUrl(),
overviewUrl: details.getOverviewUrl(),
};
}
private transformBuild = (build: buildsV1.Build): Build => {
return {
commitId: build.getCommitId(),
message: build.getMessage(),
status: statusTable[build.getStatus()] || BuildStatus.Null,
uri: build.getUri(),
};
};
}
@@ -0,0 +1,2 @@
export * from './types';
export { default as BuildsClient } from './BuildsClient';
@@ -0,0 +1,21 @@
export enum BuildStatus {
Null,
Success,
Failure,
Pending,
Running,
}
export type Build = {
commitId: string;
message: string;
status: BuildStatus;
uri: string;
};
export type BuildDetails = {
build: Build;
author: string;
logUrl: string;
overviewUrl: string;
};
@@ -1,19 +1,101 @@
import React, { FC } from 'react';
import Button from '@material-ui/core/Button';
import { BuildsClient } from '../../apis/builds';
import { useAsync } from 'react-use';
import { useRouteMatch } from 'react-router-dom';
import {
LinearProgress,
Typography,
TableContainer,
Paper,
Table,
TableBody,
TableRow,
TableCell,
Link,
makeStyles,
ButtonGroup,
Button,
} from '@material-ui/core';
type Props = {
buildId: string;
};
const useStyles = makeStyles({
root: {
maxWidth: 720,
},
});
type Props = {};
const client = BuildsClient.create('http://localhost:8080');
const BuildDetailsPage: FC<Props> = () => {
const classes = useStyles();
const match = useRouteMatch<{ buildUri: string }>();
const buildUri = decodeURIComponent(match.params.buildUri);
const status = useAsync(() => client.getBuild(buildUri), [buildUri]);
if (status.loading) {
return <LinearProgress />;
}
if (status.error) {
return (
<Typography variant="h4" color="error">
Failed to load build, {status.error}
</Typography>
);
}
const details = status.value;
const BuildDetailsPage: FC<Props> = ({ buildId }) => {
return (
<Button
variant="contained"
color="primary"
onClick={() => window.location.reload()}
>
Hello! Build details for {buildId}
</Button>
<TableContainer component={Paper} className={classes.root}>
<Table>
<TableBody>
<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>
);
};
@@ -7,39 +7,79 @@ import {
TableCell,
Paper,
TableBody,
LinearProgress,
Typography,
Tooltip,
} from '@material-ui/core';
import { RelativeEntityLink } from '@backstage/core';
import { BuildsClient } from '../../apis/builds';
import { useAsync } from 'react-use';
const client = BuildsClient.create('http://localhost:8080');
const LongText: FC<{ text: string; max: number }> = ({ text, max }) => {
if (text.length < max) {
return <span>{text}</span>;
}
return (
<Tooltip title={text}>
<span>{text.slice(0, max)}...</span>
</Tooltip>
);
};
const BuildListPage: FC<{}> = () => {
const rows = [
{ message: 'Fixed a Bar', commit: 'fb46ca3dfbd7af5bc43da', id: 165 },
{ message: 'Fixed a Foo', commit: 'd7af5bc43dafb46ca3dfb', id: 164 },
];
const status = useAsync(() => client.listBuilds('entity:spotify:backstage'));
if (status.loading) {
return <LinearProgress />;
}
if (status.error) {
return (
<Typography variant="h4" color="error">
Failed to load builds, {status.error}
</Typography>
);
}
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>
<>
<Typography variant="h4">CI/CD Builds</Typography>
<TableContainer component={Paper}>
<Table aria-label="CI/CD builds table">
<TableHead>
<TableRow>
<TableCell>Message</TableCell>
<TableCell>Commit</TableCell>
<TableCell>Status</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</TableHead>
<TableBody>
{status.value!.map(build => (
<TableRow key={build.uri}>
<TableCell>
<RelativeEntityLink
view={`builds/${encodeURIComponent(build.uri)}`}
>
<Typography color="primary">
<LongText text={build.message} max={60} />
</Typography>
</RelativeEntityLink>
</TableCell>
<TableCell>
<Tooltip title={build.commitId}>
<Typography noWrap>
{build.commitId.slice(0, 10)}
</Typography>
</Tooltip>
</TableCell>
<TableCell>{build.status}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</>
);
};
@@ -11,6 +11,6 @@ export default createPlugin({
register({ entityPage }) {
entityPage.navItem({ title: 'CI/CD', target: '/builds' });
entityPage.route('/builds', BuildListPage);
entityPage.route('/builds/:buildId', BuildDetailsPage);
entityPage.route('/builds/:buildUri', BuildDetailsPage);
},
});