Merge pull request #72 from spotify/rugvip/builds-view
frontend/plugins/github-actions: real builds list and detail views
This commit is contained in:
@@ -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;
|
||||
};
|
||||
+94
-12
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+67
-27
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user