feat: make it work

Co-authored-by: Ivan Shmidt <ivans@spotify.com>
This commit is contained in:
Nikita Nek Dudnik
2020-07-10 16:58:59 +02:00
parent 99974b710e
commit 2fd5d2ffa5
9 changed files with 110 additions and 204 deletions
@@ -16,8 +16,6 @@
import { createApiRef } from '@backstage/core';
import { Build, BuildDetails } from './types';
import { Entity } from '@backstage/catalog-model';
export const githubActionsApiRef = createApiRef<GithubActionsApi>({
id: 'plugin.githubactions.service',
@@ -25,6 +23,14 @@ export const githubActionsApiRef = createApiRef<GithubActionsApi>({
});
export type GithubActionsApi = {
listBuilds: (entity: Entity, token: Promise<string>) => Promise<Build[]>;
listBuilds: ({
owner,
repo,
token,
}: {
owner: string;
repo: string;
token: string;
}) => Promise<Build[]>;
getBuild: (buildUri: string, token: Promise<string>) => Promise<BuildDetails>;
};
@@ -16,26 +16,34 @@
import { GithubActionsApi } from './GithubActionsApi';
import { Build, BuildDetails, BuildStatus, WorkflowRun } from './types';
import { Entity } from '@backstage/catalog-model';
const statusToBuildStatus: { [status: string]: BuildStatus } = {
success: BuildStatus.Success,
failure: BuildStatus.Failure,
pending: BuildStatus.Pending,
running: BuildStatus.Running,
in_progress: BuildStatus.Running,
completed: BuildStatus.Success,
};
const conclusionToStatus = (conslusion: string): BuildStatus =>
statusToBuildStatus[conslusion] ?? BuildStatus.Null;
export class GithubActionsClient implements GithubActionsApi {
async listBuilds(entity: Entity, token: Promise<string>): Promise<Build[]> {
// ### Feedback request ###
// I asumed the following: (maybe not the best. Ideally this should come from the link to the component.yaml file)
// entity.metadata.namespace => org name
// entity.metadata.name => repo name
// entityUri -> entity:spotify:backstage
let url: string;
if (entity.metadata.name !== '') {
url = `https://api.github.com/repos/${entity.metadata.namespace}/${entity.metadata.name}/runs`;
} else {
url = 'https://api.github.com/repos/spotify/backstage/actions/runs';
}
async listBuilds({
owner,
repo,
token,
}: {
owner: string;
repo: string;
token: string;
}): Promise<Build[]> {
const url = `https://api.github.com/repos/${owner}/${repo}/actions/runs`;
const response = await fetch(url, {
headers: new Headers({
Authorization: `Bearer ${await token}`,
Authorization: `Bearer ${token}`,
}),
});
@@ -67,24 +75,7 @@ export class GithubActionsClient implements GithubActionsApi {
};
transData.commitId = String(element.head_commit.id);
transData.branch = element.head_branch;
// ### Feedback request ###
// TODO: I am not sure about this part. Looks ugly. Maybe there is a better way of doing this.
if (element.conclusion === 'success') {
transData.status = BuildStatus.Success;
} else if (element.conclusion === 'failure') {
transData.status = BuildStatus.Failure;
} else if (element.conclusion === 'pending') {
transData.status = BuildStatus.Pending;
} else if (element.conclusion === 'running') {
transData.status = BuildStatus.Running;
} else {
if (element.status === 'in_progress') {
transData.status = BuildStatus.Running;
} else {
transData.status = BuildStatus.Null;
}
}
transData.status = conclusionToStatus(element.conclusion);
transData.message = element.head_commit.message;
transData.uri = element.url;
endData[index] = transData;
@@ -130,21 +121,7 @@ export class GithubActionsClient implements GithubActionsApi {
dataBlank.build.branch = newData.head_branch;
dataBlank.build.commitId = newData.head_commit.id;
dataBlank.build.message = newData.head_commit.message;
// ### Feedback request ###
// TODO: I am not sure about this part. Look ugly. Maybe there is a better way of doing this.
if (newData.status === 'completed') {
dataBlank.build.status = BuildStatus.Success;
} else if (newData.status === 'in_progress') {
dataBlank.build.status = BuildStatus.Running;
} else if (newData.status === 'pending') {
dataBlank.build.status = BuildStatus.Pending;
} else if (newData.status === 'failure') {
dataBlank.build.status = BuildStatus.Failure;
} else {
dataBlank.build.status = BuildStatus.Null;
}
dataBlank.build.status = conclusionToStatus(newData.status);
dataBlank.build.uri = newData.url;
dataBlank.logUrl = newData.logs_url;
dataBlank.overviewUrl = newData.html_url;
@@ -1,40 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { GithubActionsApi } from './GithubActionsApi';
import { Build, BuildDetails, BuildStatus } from './types';
export class MockGithubActionsClient implements GithubActionsApi {
async listBuilds(): Promise<Build[]> {
return [];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getBuild(): Promise<BuildDetails> {
return {
build: {
commitId: 'TODO',
branch: 'TODO',
uri: 'TODO',
status: BuildStatus.Running,
message: 'TODO',
},
author: 'TODO',
logUrl: 'TODO',
overviewUrl: 'TODO',
};
}
}
@@ -1,44 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { GithubActionsClient } from './GithubActionsClient';
import { BuildStatus } from './types';
describe('Github Actions API', () => {
let client: GithubActionsClient;
beforeEach(() => {
client = new GithubActionsClient();
});
describe('Mock client', () => {
it('gets a list of builds by a project id', async () => {
await expect(client.listBuilds()).resolves.toEqual([]);
});
it('gets a build info by its id', async () => {
await expect(client.getBuild()).resolves.toEqual({
build: {
commitId: 'TODO',
branch: 'TODO',
uri: 'TODO',
status: BuildStatus.Running,
message: 'TODO',
},
author: 'TODO',
logUrl: 'TODO',
overviewUrl: 'TODO',
});
});
});
});
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Link } from '@backstage/core';
import {
Button,
ButtonGroup,
@@ -30,10 +29,10 @@ import {
Typography,
} from '@material-ui/core';
import React from 'react';
import { useParams } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import { useAsync } from 'react-use';
import { BuildStatusIndicator } from '../BuildStatusIndicator';
import { useApi, githubAuthApiRef } from '@backstage/core-api';
import { Link, useApi, githubAuthApiRef } from '@backstage/core';
import { githubActionsApiRef } from '../../api';
const useStyles = makeStyles<Theme>(theme => ({
@@ -55,8 +54,12 @@ export const BuildDetailsPage = () => {
const token = githubApi.getAccessToken('repo');
const classes = useStyles();
const { buildUri } = useParams();
const status = useAsync(() => api.getBuild(buildUri, token), [buildUri]);
const location = useLocation();
const status = useAsync(
() =>
api.getBuild(decodeURIComponent(location.search.split('uri=')[1]), token),
[location.search],
);
if (status.loading) {
return <LinearProgress />;
@@ -73,7 +76,7 @@ export const BuildDetailsPage = () => {
return (
<div className={classes.root}>
<Typography className={classes.title} variant="h3">
<Link to="/builds">
<Link to="/github-actions">
<Typography component="span" variant="h3" color="primary">
&lt;
</Typography>
@@ -127,12 +130,12 @@ export const BuildDetailsPage = () => {
>
{details?.overviewUrl && (
<Button>
<Link to={details.overviewUrl}>GitHub</Link>
<a href={details.overviewUrl}>GitHub</a>
</Button>
)}
{details?.logUrl && (
<Button>
<Link to={details.logUrl}>Logs</Link>
<a href={details.logUrl}>Logs</a>
</Button>
)}
</ButtonGroup>
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Link } from '@backstage/core';
import {
LinearProgress,
makeStyles,
@@ -29,7 +28,7 @@ import React from 'react';
import { useAsync } from 'react-use';
import { BuildStatusIndicator } from '../BuildStatusIndicator';
import { githubActionsApiRef } from '../../api';
import { useApi } from '@backstage/core-api';
import { Link, useApi, githubAuthApiRef } from '@backstage/core';
const useStyles = makeStyles<Theme>(theme => ({
root: {
@@ -40,63 +39,69 @@ const useStyles = makeStyles<Theme>(theme => ({
},
}));
export const BuildInfoCard = () => {
const classes = useStyles();
const BuildInfoCardContent = () => {
const api = useApi(githubActionsApiRef);
const status = useAsync(() => api.listBuilds('entity:spotify:backstage'));
const githubApi = useApi(githubAuthApiRef);
let content: JSX.Element;
const status = useAsync(async () => {
const token = await githubApi.getAccessToken('repo');
return api.listBuilds({ owner: 'spotify', repo: 'backstage', token });
});
if (status.loading) {
content = <LinearProgress />;
return <LinearProgress />;
} else if (status.error) {
content = (
return (
<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>
<Link to={`builds/${encodeURIComponent(build?.uri || '')}`}>
<Typography color="primary">{build?.message}</Typography>
</Link>
</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>
);
}
const [build] =
status.value?.filter(({ branch }) => branch === 'master') ?? [];
return (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Typography noWrap>Message</Typography>
</TableCell>
<TableCell>
<Link to={`builds/${encodeURIComponent(build?.uri || '')}`}>
<Typography color="primary">{build?.message}</Typography>
</Link>
</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>
);
};
export const BuildInfoCard = () => {
const classes = useStyles();
return (
<div className={classes.root}>
<Typography variant="h2" className={classes.title}>
Master Build
</Typography>
{content}
<BuildInfoCardContent />
</div>
);
};
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Link } from '@backstage/core';
import { Link, useApi, githubAuthApiRef } from '@backstage/core';
import {
LinearProgress,
makeStyles,
@@ -29,13 +29,10 @@ import {
Tooltip,
Typography,
} from '@material-ui/core';
import React, { FC } from 'react';
import React from 'react';
import { useAsync } from 'react-use';
import { BuildStatusIndicator } from '../BuildStatusIndicator';
import { githubActionsApiRef } from '../../api';
import { useApi, githubAuthApiRef } from '@backstage/core-api';
import { Entity } from '@backstage/catalog-model';
import { githubActionsApiRef, Build } from '../../api';
const LongText = ({ text, max }: { text: string; max: number }) => {
if (text.length < max) {
@@ -57,14 +54,15 @@ const useStyles = makeStyles<Theme>(theme => ({
},
}));
const PageContents: FC<{ entity: Entity }> = ({ entity }) => {
const PageContents = ({ owner, repo }: { owner: string; repo: string }) => {
const api = useApi(githubActionsApiRef);
const githubApi = useApi(githubAuthApiRef);
const token = githubApi.getAccessToken('repo');
const { loading, error, value } = useAsync(() =>
api.listBuilds(entity, token),
);
const { loading, error, value } = useAsync(async () => {
const token = await githubApi.getAccessToken('repo');
return api.listBuilds({ owner, repo, token });
}, [githubApi, owner, repo]);
if (loading) {
return <LinearProgress />;
@@ -90,7 +88,7 @@ const PageContents: FC<{ entity: Entity }> = ({ entity }) => {
</TableRow>
</TableHead>
<TableBody>
{value!.map(build => (
{value?.map((build: Build) => (
<TableRow key={build.uri}>
<TableCell>
<BuildStatusIndicator status={build.status} />
@@ -101,7 +99,7 @@ const PageContents: FC<{ entity: Entity }> = ({ entity }) => {
</Typography>
</TableCell>
<TableCell>
<Link to={`builds/${encodeURIComponent(build.uri)}`}>
<Link to={`builds?uri=${encodeURIComponent(build.uri)}`}>
<Typography color="primary">
<LongText text={build.message} max={60} />
</Typography>
@@ -120,14 +118,15 @@ const PageContents: FC<{ entity: Entity }> = ({ entity }) => {
);
};
export const BuildListPage: FC<{ entity: Entity }> = ({ entity }) => {
export const BuildListPage = () => {
const classes = useStyles();
return (
<div className={classes.root}>
<Typography variant="h3" className={classes.title}>
CI/CD Builds
</Typography>
<PageContents entity={entity} />
<PageContents owner="spotify" repo="backstage" />
</div>
);
};
+1 -1
View File
@@ -24,7 +24,7 @@ export const rootRouteRef = createRouteRef({
title: 'GitHub Actions',
});
export const buildRouteRef = createRouteRef({
path: '/github-actions/builds/:buildUri',
path: '/github-actions/builds',
title: 'GitHub Actions Build',
});