feat: github actions details view
This commit is contained in:
@@ -72,5 +72,5 @@ export type GithubActionsApi = {
|
||||
owner: string;
|
||||
repo: string;
|
||||
runId: number;
|
||||
}) => void;
|
||||
}) => Promise<any>;
|
||||
};
|
||||
|
||||
@@ -33,8 +33,8 @@ export class GithubActionsClient implements GithubActionsApi {
|
||||
owner: string;
|
||||
repo: string;
|
||||
runId: number;
|
||||
}) {
|
||||
new Octokit({ auth: token }).actions.reRunWorkflow({
|
||||
}): Promise<any> {
|
||||
return new Octokit({ auth: token }).actions.reRunWorkflow({
|
||||
owner,
|
||||
repo,
|
||||
run_id: runId,
|
||||
|
||||
@@ -1,143 +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 {
|
||||
Button,
|
||||
LinearProgress,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
Theme,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import { Link, useApi, githubAuthApiRef } from '@backstage/core';
|
||||
import { githubActionsApiRef } from '../../api';
|
||||
|
||||
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),
|
||||
},
|
||||
}));
|
||||
|
||||
export const BuildDetailsPage = () => {
|
||||
const repo = 'try-ssr';
|
||||
const owner = 'CircleCITest3';
|
||||
const api = useApi(githubActionsApiRef);
|
||||
const auth = useApi(githubAuthApiRef);
|
||||
|
||||
const classes = useStyles();
|
||||
const { id } = useParams();
|
||||
const status = useAsync(async () => {
|
||||
const token = await auth.getAccessToken(['repo', 'user']);
|
||||
return api
|
||||
.getWorkflowRun({
|
||||
token,
|
||||
owner,
|
||||
repo,
|
||||
id: parseInt(id, 10),
|
||||
})
|
||||
.then(data => {
|
||||
return data;
|
||||
});
|
||||
}, [location.search]);
|
||||
|
||||
if (status.loading) {
|
||||
return <LinearProgress />;
|
||||
} else if (status.error) {
|
||||
return (
|
||||
<Typography variant="h6" color="error">
|
||||
Failed to load build, {status.error.message}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
const details = status.value;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Typography className={classes.title} variant="h3">
|
||||
<Link to="/github-actions">
|
||||
<Typography component="span" variant="h3" color="primary">
|
||||
<
|
||||
</Typography>
|
||||
</Link>
|
||||
Build Details
|
||||
</Typography>
|
||||
<TableContainer component={Paper} className={classes.table}>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Branch</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details?.head_branch}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Message</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details?.head_commit.message}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Commit ID</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details?.head_commit.id}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Status</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details?.status}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Author</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{`${details?.head_commit.author.name} (${details?.head_commit.author.email})`}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Links</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{details?.html_url && (
|
||||
<Button>
|
||||
<a href={details.html_url}>GitHub</a>
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,107 +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 {
|
||||
LinearProgress,
|
||||
makeStyles,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
Theme,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { BuildStatusIndicator } from '../BuildStatusIndicator';
|
||||
import { githubActionsApiRef, BuildStatus } from '../../api';
|
||||
import { Link, useApi, githubAuthApiRef } from '@backstage/core';
|
||||
|
||||
const useStyles = makeStyles<Theme>(theme => ({
|
||||
root: {
|
||||
// height: 400,
|
||||
},
|
||||
title: {
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
const BuildInfoCardContent = () => {
|
||||
const api = useApi(githubActionsApiRef);
|
||||
const auth = useApi(githubAuthApiRef);
|
||||
|
||||
const status = useAsync(async () => {
|
||||
const token = await auth.getAccessToken(['repo', 'user']);
|
||||
return api.listWorkflowRuns({ token, owner: 'spotify', repo: 'backstage' });
|
||||
});
|
||||
|
||||
if (status.loading) {
|
||||
return <LinearProgress />;
|
||||
} else if (status.error) {
|
||||
return (
|
||||
<Typography variant="h2" color="error">
|
||||
Failed to load builds, {status.error.message}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
// const [build] =
|
||||
// status.value?.filter(({ branch }) => branch === 'master') ?? [];
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Message</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Link to="builds/123">
|
||||
<Typography color="primary">build message</Typography>
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Commit ID</Typography>
|
||||
</TableCell>
|
||||
<TableCell>build commit id</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Status</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<BuildStatusIndicator status={BuildStatus.Success} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export const BuildInfoCard = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Typography variant="h2" className={classes.title}>
|
||||
Master Build
|
||||
</Typography>
|
||||
<BuildInfoCardContent />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* 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 {
|
||||
Button,
|
||||
LinearProgress,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
Theme,
|
||||
Typography,
|
||||
Box,
|
||||
ExpansionPanelDetails,
|
||||
ExpansionPanel,
|
||||
ExpansionPanelSummary,
|
||||
ListItem,
|
||||
List,
|
||||
ListItemText,
|
||||
CircularProgress,
|
||||
} from '@material-ui/core';
|
||||
import moment from 'moment';
|
||||
|
||||
import React from 'react';
|
||||
import { Link, useApi, configApiRef } from '@backstage/core';
|
||||
import { Job, Step, Jobs } from '../types';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { WorkflowRunStatusIndicator } from '../WorkflowRunStatusIndicator';
|
||||
import { useWorkflowRunsDetails } from './useWorkflowRunsDetails';
|
||||
import { useWorkflowRunJobs } from './useWorkflowRunJobs';
|
||||
|
||||
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),
|
||||
},
|
||||
expansionPanelDetails: {
|
||||
padding: 0,
|
||||
},
|
||||
button: {
|
||||
order: -1,
|
||||
marginRight: 0,
|
||||
marginLeft: '-20px',
|
||||
},
|
||||
}));
|
||||
|
||||
const JobsList = ({ jobs }: { jobs?: Jobs }) => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Box>
|
||||
{jobs &&
|
||||
jobs.total_count > 0 &&
|
||||
jobs.jobs.map((job: Job) => (
|
||||
<JobListItem
|
||||
job={job}
|
||||
className={
|
||||
job.status !== 'success' ? classes.failed : classes.success
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const getElapsedTime = (start: string, end: string) => {
|
||||
const diff = moment(moment(end || moment()).diff(moment(start)));
|
||||
const timeElapsed = diff.format('m [minutes] s [seconds]');
|
||||
return timeElapsed;
|
||||
};
|
||||
|
||||
const StepView = ({ step }: { step: Step }) => {
|
||||
return (
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={step.name}
|
||||
secondary={getElapsedTime(step.started_at, step.completed_at)}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
const JobListItem = ({ job, className }: { job: Job; className: string }) => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<ExpansionPanel
|
||||
TransitionProps={{ unmountOnExit: true }}
|
||||
className={className}
|
||||
>
|
||||
<ExpansionPanelSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls={`panel-${name}-content`}
|
||||
id={`panel-${name}-header`}
|
||||
IconButtonProps={{
|
||||
className: classes.button,
|
||||
}}
|
||||
>
|
||||
<Typography variant="button">
|
||||
{job.name} ({getElapsedTime(job.started_at, job.completed_at)})
|
||||
</Typography>
|
||||
</ExpansionPanelSummary>
|
||||
<ExpansionPanelDetails className={classes.expansionPanelDetails}>
|
||||
<List>
|
||||
{job.steps.map((step: Step) => (
|
||||
<StepView step={step} />
|
||||
))}
|
||||
</List>
|
||||
</ExpansionPanelDetails>
|
||||
</ExpansionPanel>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A component for Jobs visualization. Jobs are a property of a Workflow Run.
|
||||
*/
|
||||
export const WorkflowRunDetailsPage = () => {
|
||||
const configApi = useApi(configApiRef);
|
||||
const repo = configApi.getString('github-actions.repo');
|
||||
const owner = configApi.getString('github-actions.owner');
|
||||
const details = useWorkflowRunsDetails(repo, owner);
|
||||
const jobs = useWorkflowRunJobs(details.value?.jobs_url);
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
if (details.loading) {
|
||||
return <LinearProgress />;
|
||||
} else if (details.error) {
|
||||
return (
|
||||
<Typography variant="h6" color="error">
|
||||
Failed to load build, {details.error.message}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Typography className={classes.title} variant="h3">
|
||||
<Link to="/github-actions">
|
||||
<Typography component="span" variant="h3" color="primary">
|
||||
<
|
||||
</Typography>
|
||||
</Link>
|
||||
Workflow Run Details
|
||||
</Typography>
|
||||
<TableContainer component={Paper} className={classes.table}>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Branch</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details.value?.head_branch}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Message</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details.value?.head_commit.message}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Commit ID</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details.value?.head_commit.id}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Status</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<WorkflowRunStatusIndicator status={details.value?.status} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Author</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{`${details.value?.head_commit.author.name} (${details.value?.head_commit.author.email})`}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Links</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{details.value?.html_url && (
|
||||
<Button>
|
||||
<a href={details.value.html_url}>GitHub</a>
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={2}>
|
||||
<Typography noWrap>Jobs</Typography>
|
||||
{jobs.loading ? (
|
||||
<CircularProgress />
|
||||
) : (
|
||||
<JobsList jobs={jobs.value} />
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { BuildDetailsPage } from './BuildDetailsPage';
|
||||
export { WorkflowRunDetailsPage } from './WorkflowRunDetailsPage';
|
||||
+10
-1
@@ -13,5 +13,14 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { useAsync } from 'react-use';
|
||||
import { Jobs } from '../types';
|
||||
|
||||
export { BuildInfoCard } from './BuildInfoCard';
|
||||
export const useWorkflowRunJobs = (jobsUrl?: string) => {
|
||||
const jobs = useAsync<Jobs>(async () => {
|
||||
if (jobsUrl === undefined) return [];
|
||||
const data = await fetch(jobsUrl).then(d => d.json());
|
||||
return data;
|
||||
}, [jobsUrl]);
|
||||
return jobs;
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 { useApi, githubAuthApiRef } from '@backstage/core';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import { githubActionsApiRef } from '../../api';
|
||||
|
||||
export const useWorkflowRunsDetails = (repo: string, owner: string) => {
|
||||
const api = useApi(githubActionsApiRef);
|
||||
const auth = useApi(githubAuthApiRef);
|
||||
const { id } = useParams();
|
||||
const details = useAsync(async () => {
|
||||
const token = await auth.getAccessToken(['repo', 'user']);
|
||||
return api.getWorkflowRun({
|
||||
token,
|
||||
owner,
|
||||
repo,
|
||||
id: parseInt(id, 10),
|
||||
});
|
||||
}, [id]);
|
||||
return details;
|
||||
};
|
||||
+23
-18
@@ -15,16 +15,14 @@
|
||||
*/
|
||||
|
||||
import { IconComponent } from '@backstage/core';
|
||||
import { makeStyles, Theme } from '@material-ui/core';
|
||||
import { makeStyles, Theme, Typography } from '@material-ui/core';
|
||||
import ProgressIcon from '@material-ui/icons/Autorenew';
|
||||
import SuccessIcon from '@material-ui/icons/CheckCircle';
|
||||
import FailureIcon from '@material-ui/icons/Error';
|
||||
import UnknownIcon from '@material-ui/icons/Help';
|
||||
import React from 'react';
|
||||
import { BuildStatus } from '../../api/types';
|
||||
|
||||
type Props = {
|
||||
status?: BuildStatus;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
type StatusStyle = {
|
||||
@@ -32,43 +30,50 @@ type StatusStyle = {
|
||||
color: string;
|
||||
};
|
||||
|
||||
const styles: { [key in BuildStatus]: StatusStyle } = {
|
||||
[BuildStatus.Null]: {
|
||||
const styles: { [key: string]: StatusStyle } = {
|
||||
unknown: {
|
||||
icon: UnknownIcon,
|
||||
color: '#f49b20',
|
||||
},
|
||||
[BuildStatus.Success]: {
|
||||
completed: {
|
||||
icon: SuccessIcon,
|
||||
color: '#1db855',
|
||||
},
|
||||
[BuildStatus.Failure]: {
|
||||
icon: FailureIcon,
|
||||
color: '#CA001B',
|
||||
},
|
||||
[BuildStatus.Pending]: {
|
||||
queued: {
|
||||
icon: UnknownIcon,
|
||||
color: '#5BC0DE',
|
||||
},
|
||||
[BuildStatus.Running]: {
|
||||
in_progress: {
|
||||
icon: ProgressIcon,
|
||||
color: '#BEBEBE',
|
||||
},
|
||||
};
|
||||
|
||||
const getIconStyle = (status?: string): StatusStyle => {
|
||||
return styles[status ?? 'unknown'] ?? styles.unknown;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles<Theme, StatusStyle>({
|
||||
icon: style => ({
|
||||
color: style.color,
|
||||
}),
|
||||
});
|
||||
|
||||
export const BuildStatusIndicator = ({ status }: Props) => {
|
||||
const style = (status && styles[status]) || styles[BuildStatus.Null];
|
||||
export const WorkflowRunStatusIndicator = ({ status }: Props) => {
|
||||
const style = getIconStyle(status);
|
||||
const classes = useStyles(style);
|
||||
const Icon = style.icon;
|
||||
|
||||
return (
|
||||
<div className={classes.icon}>
|
||||
<Icon />
|
||||
</div>
|
||||
<Typography
|
||||
className={classes.icon}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
textTransform: 'capitalize',
|
||||
}}
|
||||
>
|
||||
<Icon /> {`${status}`}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { BuildStatusIndicator } from './BuildStatusIndicator';
|
||||
export { WorkflowRunStatusIndicator } from './WorkflowRunStatusIndicator';
|
||||
+10
-5
@@ -22,24 +22,29 @@ import {
|
||||
Content,
|
||||
ContentHeader,
|
||||
SupportButton,
|
||||
useApi,
|
||||
configApiRef,
|
||||
} from '@backstage/core';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
|
||||
import { BuildListTable } from '../BuildListTable';
|
||||
import { WorkflowRunsTable } from '../WorkflowRunsTable';
|
||||
|
||||
export const BuildListPage = () => {
|
||||
export const WorkflowRunsPage = () => {
|
||||
const configApi = useApi(configApiRef);
|
||||
const repo = configApi.getString('github-actions.repo');
|
||||
const owner = configApi.getString('github-actions.owner');
|
||||
return (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
title="GitHub Actions"
|
||||
subtitle="See recent builds and their status"
|
||||
subtitle="See recent worflow runs and their status"
|
||||
>
|
||||
<HeaderLabel label="Owner" value="Spotify" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentHeader title="All builds">
|
||||
<ContentHeader title="Workflow runs">
|
||||
<SupportButton>
|
||||
This plugin allows you to view and interact with your builds within
|
||||
the GitHub Actions environment.
|
||||
@@ -47,7 +52,7 @@ export const BuildListPage = () => {
|
||||
</ContentHeader>
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<BuildListTable repo="try-ssr" owner="CircleCITest3" />
|
||||
<WorkflowRunsTable repo={repo} owner={owner} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { BuildListPage } from './BuildListPage';
|
||||
export { WorkflowRunsPage } from './WorkflowRunsPage';
|
||||
+27
-30
@@ -19,20 +19,18 @@ import RetryIcon from '@material-ui/icons/Replay';
|
||||
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import {
|
||||
StatusError,
|
||||
StatusWarning,
|
||||
StatusOK,
|
||||
StatusPending,
|
||||
StatusRunning,
|
||||
Table,
|
||||
TableColumn,
|
||||
} from '@backstage/core';
|
||||
import { useBuilds } from './useBuilds';
|
||||
import { useWorkflowRuns } from './useWorkflowRuns';
|
||||
|
||||
export type Build = {
|
||||
export type WorkflowRun = {
|
||||
id: string;
|
||||
buildName: string;
|
||||
buildUrl?: string;
|
||||
message: string;
|
||||
url?: string;
|
||||
source: {
|
||||
branchName: string;
|
||||
commit: {
|
||||
@@ -41,24 +39,20 @@ export type Build = {
|
||||
};
|
||||
};
|
||||
status: string;
|
||||
onRestartClick: () => void;
|
||||
onReRunClick: () => void;
|
||||
};
|
||||
|
||||
// retried, canceled, infrastructure_fail, timedout, not_run, running, failed, queued, scheduled, not_running, no_tests, fixed, success
|
||||
const getStatusComponent = (status: string | undefined = '') => {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'queued':
|
||||
case 'scheduled':
|
||||
return <StatusPending />;
|
||||
case 'running':
|
||||
case 'in_progress':
|
||||
return <StatusRunning />;
|
||||
case 'failed':
|
||||
return <StatusError />;
|
||||
case 'success':
|
||||
case 'completed':
|
||||
return <StatusOK />;
|
||||
case 'canceled':
|
||||
default:
|
||||
return <StatusWarning />;
|
||||
return <StatusPending />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,18 +64,21 @@ const generatedColumns: TableColumn[] = [
|
||||
width: '150px',
|
||||
},
|
||||
{
|
||||
title: 'Build',
|
||||
field: 'buildName',
|
||||
title: 'Message',
|
||||
field: 'message',
|
||||
highlight: true,
|
||||
render: (row: Partial<Build>) => (
|
||||
<Link component={RouterLink} to={`/github-actions/build/${row.id}`}>
|
||||
{row.buildName}
|
||||
render: (row: Partial<WorkflowRun>) => (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`/github-actions/workflow-run/${row.id}`}
|
||||
>
|
||||
{row.message}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Source',
|
||||
render: (row: Partial<Build>) => (
|
||||
render: (row: Partial<WorkflowRun>) => (
|
||||
<>
|
||||
<p>{row.source?.branchName}</p>
|
||||
<p>{row.source?.commit.hash}</p>
|
||||
@@ -90,7 +87,7 @@ const generatedColumns: TableColumn[] = [
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
render: (row: Partial<Build>) => (
|
||||
render: (row: Partial<WorkflowRun>) => (
|
||||
<Box display="flex" alignItems="center">
|
||||
{getStatusComponent(row.status)}
|
||||
<Box mr={1} />
|
||||
@@ -100,8 +97,8 @@ const generatedColumns: TableColumn[] = [
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
render: (row: Partial<Build>) => (
|
||||
<IconButton onClick={row.onRestartClick}>
|
||||
render: (row: Partial<WorkflowRun>) => (
|
||||
<IconButton onClick={row.onReRunClick}>
|
||||
<RetryIcon />
|
||||
</IconButton>
|
||||
),
|
||||
@@ -112,7 +109,7 @@ const generatedColumns: TableColumn[] = [
|
||||
type Props = {
|
||||
loading: boolean;
|
||||
retry: () => void;
|
||||
builds?: Build[];
|
||||
runs?: WorkflowRun[];
|
||||
projectName: string;
|
||||
page: number;
|
||||
onChangePage: (page: number) => void;
|
||||
@@ -121,13 +118,13 @@ type Props = {
|
||||
onChangePageSize: (pageSize: number) => void;
|
||||
};
|
||||
|
||||
const BuildListTableView: FC<Props> = ({
|
||||
const WorkflowRunsTableView: FC<Props> = ({
|
||||
projectName,
|
||||
loading,
|
||||
pageSize,
|
||||
page,
|
||||
retry,
|
||||
builds,
|
||||
runs,
|
||||
onChangePage,
|
||||
onChangePageSize,
|
||||
total,
|
||||
@@ -146,7 +143,7 @@ const BuildListTableView: FC<Props> = ({
|
||||
onClick: () => retry(),
|
||||
},
|
||||
]}
|
||||
data={builds ?? []}
|
||||
data={runs ?? []}
|
||||
onChangePage={onChangePage}
|
||||
onChangeRowsPerPage={onChangePageSize}
|
||||
title={
|
||||
@@ -161,19 +158,19 @@ const BuildListTableView: FC<Props> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const BuildListTable = ({
|
||||
export const WorkflowRunsTable = ({
|
||||
repo,
|
||||
owner,
|
||||
}: {
|
||||
repo: string;
|
||||
owner: string;
|
||||
}) => {
|
||||
const [tableProps, { retry, setPage, setPageSize }] = useBuilds({
|
||||
const [tableProps, { retry, setPage, setPageSize }] = useWorkflowRuns({
|
||||
repo,
|
||||
owner,
|
||||
});
|
||||
return (
|
||||
<BuildListTableView
|
||||
<WorkflowRunsTableView
|
||||
{...tableProps}
|
||||
retry={retry}
|
||||
onChangePageSize={setPageSize}
|
||||
+2
-2
@@ -13,5 +13,5 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { BuildListTable } from './BuildListTable';
|
||||
export type { Build } from './BuildListTable';
|
||||
export { WorkflowRunsTable } from './WorkflowRunsTable';
|
||||
export type { WorkflowRun } from './WorkflowRunsTable';
|
||||
+36
-20
@@ -15,22 +15,32 @@
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useAsyncRetry } from 'react-use';
|
||||
import { Build } from './BuildListTable';
|
||||
import { WorkflowRun } from './WorkflowRunsTable';
|
||||
import { githubActionsApiRef } from '../../api/GithubActionsApi';
|
||||
import { useApi, githubAuthApiRef } from '@backstage/core';
|
||||
import { useApi, githubAuthApiRef, errorApiRef } from '@backstage/core';
|
||||
import { ActionsListWorkflowRunsForRepoResponseData } from '@octokit/types';
|
||||
|
||||
export function useBuilds({ repo, owner }: { repo: string; owner: string }) {
|
||||
export function useWorkflowRuns({
|
||||
repo,
|
||||
owner,
|
||||
}: {
|
||||
repo: string;
|
||||
owner: string;
|
||||
}) {
|
||||
const api = useApi(githubActionsApiRef);
|
||||
const auth = useApi(githubAuthApiRef);
|
||||
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
|
||||
const restartBuild = async () => {};
|
||||
const reRunWorkflow = async () => {};
|
||||
|
||||
const { loading, value: builds, retry } = useAsyncRetry<Build[]>(async () => {
|
||||
const { loading, value: runs, retry } = useAsyncRetry<
|
||||
WorkflowRun[]
|
||||
>(async () => {
|
||||
const token = await auth.getAccessToken(['repo', 'user']);
|
||||
|
||||
return (
|
||||
@@ -38,19 +48,25 @@ export function useBuilds({ repo, owner }: { repo: string; owner: string }) {
|
||||
// GitHub API pagination count starts from 1
|
||||
.listWorkflowRuns({ token, owner, repo, pageSize, page: page + 1 })
|
||||
.then(
|
||||
(allBuilds: ActionsListWorkflowRunsForRepoResponseData): Build[] => {
|
||||
setTotal(allBuilds.total_count);
|
||||
(
|
||||
workflowRunsData: ActionsListWorkflowRunsForRepoResponseData,
|
||||
): WorkflowRun[] => {
|
||||
setTotal(workflowRunsData.total_count);
|
||||
// Transformation here
|
||||
return allBuilds.workflow_runs.map(run => ({
|
||||
buildName: run.head_commit.message,
|
||||
return workflowRunsData.workflow_runs.map(run => ({
|
||||
message: run.head_commit.message,
|
||||
id: `${run.id}`,
|
||||
onRestartClick: () => {
|
||||
api.reRunWorkflow({
|
||||
token,
|
||||
owner,
|
||||
repo,
|
||||
runId: run.id,
|
||||
});
|
||||
onReRunClick: async () => {
|
||||
try {
|
||||
await api.reRunWorkflow({
|
||||
token,
|
||||
owner,
|
||||
repo,
|
||||
runId: run.id,
|
||||
});
|
||||
} catch (e) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
},
|
||||
source: {
|
||||
branchName: run.head_branch,
|
||||
@@ -63,7 +79,7 @@ export function useBuilds({ repo, owner }: { repo: string; owner: string }) {
|
||||
},
|
||||
},
|
||||
status: run.status,
|
||||
buildUrl: run.url,
|
||||
url: run.url,
|
||||
}));
|
||||
},
|
||||
)
|
||||
@@ -76,15 +92,15 @@ export function useBuilds({ repo, owner }: { repo: string; owner: string }) {
|
||||
page,
|
||||
pageSize,
|
||||
loading,
|
||||
builds,
|
||||
runs,
|
||||
projectName,
|
||||
total,
|
||||
},
|
||||
{
|
||||
builds,
|
||||
runs,
|
||||
setPage,
|
||||
setPageSize,
|
||||
restartBuild,
|
||||
reRunWorkflow,
|
||||
retry,
|
||||
},
|
||||
] as const;
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export type Step = {
|
||||
name: string;
|
||||
status: string;
|
||||
conclusion: string;
|
||||
number: number; // starts from 1
|
||||
started_at: string;
|
||||
completed_at: string;
|
||||
};
|
||||
|
||||
export type Job = {
|
||||
html_url: string;
|
||||
status: string;
|
||||
conclusion: string;
|
||||
started_at: string;
|
||||
completed_at: string;
|
||||
name: string;
|
||||
steps: Step[];
|
||||
};
|
||||
|
||||
export type Jobs = {
|
||||
total_count: number;
|
||||
jobs: Job[];
|
||||
};
|
||||
|
||||
export enum BuildStatus {
|
||||
'success',
|
||||
'failure',
|
||||
'pending',
|
||||
'running',
|
||||
}
|
||||
@@ -15,8 +15,8 @@
|
||||
*/
|
||||
|
||||
import { createPlugin, createRouteRef } from '@backstage/core';
|
||||
import { BuildDetailsPage } from './components/BuildDetailsPage';
|
||||
import { BuildListPage } from './components/BuildListPage';
|
||||
import { WorkflowRunDetailsPage } from './components/WorkflowRunDetailsPage';
|
||||
import { WorkflowRunsPage } from './components/WorkflowRunsPage';
|
||||
|
||||
// TODO(freben): This is just a demo route for now
|
||||
export const rootRouteRef = createRouteRef({
|
||||
@@ -24,14 +24,14 @@ export const rootRouteRef = createRouteRef({
|
||||
title: 'GitHub Actions',
|
||||
});
|
||||
export const buildRouteRef = createRouteRef({
|
||||
path: '/github-actions/build/:id',
|
||||
title: 'GitHub Actions Build',
|
||||
path: '/github-actions/workflow-run/:id',
|
||||
title: 'GitHub Actions Workflow Run',
|
||||
});
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'github-actions',
|
||||
register({ router }) {
|
||||
router.addRoute(rootRouteRef, BuildListPage);
|
||||
router.addRoute(buildRouteRef, BuildDetailsPage);
|
||||
router.addRoute(rootRouteRef, WorkflowRunsPage);
|
||||
router.addRoute(buildRouteRef, WorkflowRunDetailsPage);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user