@@ -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">
|
||||
<
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user