Merge remote-tracking branch 'upstream/master'

This commit is contained in:
NetPenguins
2020-08-30 09:45:59 -04:00
894 changed files with 39109 additions and 6519 deletions
@@ -18,13 +18,12 @@ import { createApiRef } from '@backstage/core';
import {
ActionsListWorkflowRunsForRepoResponseData,
ActionsGetWorkflowResponseData,
ActionsGetWorkflowRunResponseData,
ActionsListJobsForWorkflowRunResponseData
ActionsGetWorkflowRunResponseData
} from '@octokit/types';
export const githubActionsApiRef = createApiRef<GithubActionsApi>({
id: 'plugin.githubactions.service',
description: 'Used by the Github Actions plugin to make requests',
description: 'Used by the GitHub Actions plugin to make requests',
});
export type GithubActionsApi = {
@@ -34,12 +33,14 @@ export type GithubActionsApi = {
repo,
pageSize,
page,
branch,
}: {
token: string;
owner: string;
repo: string;
pageSize?: number;
page?: number;
branch?: string;
}) => Promise<ActionsListWorkflowRunsForRepoResponseData>;
getWorkflow: ({
token,
@@ -73,16 +74,5 @@ export type GithubActionsApi = {
owner: string;
repo: string;
runId: number;
}) => void;
listJobsForWorkflowRun: ({
token,
owner,
repo,
id,
}: {
token: string;
owner: string;
repo: string;
id: number;
}) => Promise<ActionsListJobsForWorkflowRunResponseData>;
}) => Promise<any>;
};
@@ -24,7 +24,7 @@ import {
} from '@octokit/types';
export class GithubActionsClient implements GithubActionsApi {
reRunWorkflow({
async reRunWorkflow({
token,
owner,
repo,
@@ -34,8 +34,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,
@@ -47,12 +47,14 @@ export class GithubActionsClient implements GithubActionsApi {
repo,
pageSize = 100,
page = 0,
branch,
}: {
token: string;
owner: string;
repo: string;
pageSize?: number;
page?: number;
branch?: string;
}): Promise<ActionsListWorkflowRunsForRepoResponseData> {
const workflowRuns = await new Octokit({
auth: token,
@@ -61,6 +63,7 @@ export class GithubActionsClient implements GithubActionsApi {
repo,
per_page: pageSize,
page,
...(branch ? { branch } : {}),
});
return workflowRuns.data;
}
+25 -203
View File
@@ -14,211 +14,33 @@
* limitations under the License.
*/
export enum BuildStatus {
Null,
Success,
Failure,
Pending,
Running,
}
export type Build = {
commitId: string;
message: string;
branch: string;
status: BuildStatus;
uri: string;
};
export type BuildDetails = {
build: Build;
author: string;
logUrl: string;
overviewUrl: string;
};
export interface Author {
export type Step = {
name: string;
email: string;
}
export interface Committer {
name: string;
email: string;
}
export interface HeadCommit {
id: string;
tree_id: string;
message: string;
timestamp: Date;
author: Author;
committer: Committer;
}
export interface Owner {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: boolean;
}
export interface Repository {
id: number;
node_id: string;
name: string;
full_name: string;
private: boolean;
owner: Owner;
html_url: string;
description?: any;
fork: boolean;
url: string;
forks_url: string;
keys_url: string;
collaborators_url: string;
teams_url: string;
hooks_url: string;
issue_events_url: string;
events_url: string;
assignees_url: string;
branches_url: string;
tags_url: string;
blobs_url: string;
git_tags_url: string;
git_refs_url: string;
trees_url: string;
statuses_url: string;
languages_url: string;
stargazers_url: string;
contributors_url: string;
subscribers_url: string;
subscription_url: string;
commits_url: string;
git_commits_url: string;
comments_url: string;
issue_comment_url: string;
contents_url: string;
compare_url: string;
merges_url: string;
archive_url: string;
downloads_url: string;
issues_url: string;
pulls_url: string;
milestones_url: string;
notifications_url: string;
labels_url: string;
releases_url: string;
deployments_url: string;
}
export interface Owner2 {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: boolean;
}
export interface HeadRepository {
id: number;
node_id: string;
name: string;
full_name: string;
private: boolean;
owner: Owner2;
html_url: string;
description?: any;
fork: boolean;
url: string;
forks_url: string;
keys_url: string;
collaborators_url: string;
teams_url: string;
hooks_url: string;
issue_events_url: string;
events_url: string;
assignees_url: string;
branches_url: string;
tags_url: string;
blobs_url: string;
git_tags_url: string;
git_refs_url: string;
trees_url: string;
statuses_url: string;
languages_url: string;
stargazers_url: string;
contributors_url: string;
subscribers_url: string;
subscription_url: string;
commits_url: string;
git_commits_url: string;
comments_url: string;
issue_comment_url: string;
contents_url: string;
compare_url: string;
merges_url: string;
archive_url: string;
downloads_url: string;
issues_url: string;
pulls_url: string;
milestones_url: string;
notifications_url: string;
labels_url: string;
releases_url: string;
deployments_url: string;
}
export interface WorkflowRun {
id: number;
node_id: string;
head_branch: string;
head_sha: string;
run_number: number;
event: string;
status: string;
conclusion: string;
workflow_id: number;
url: string;
number: number; // starts from 1
started_at: string;
completed_at: string;
};
export type Job = {
html_url: string;
pull_requests: any[];
created_at: Date;
updated_at: Date;
jobs_url: string;
logs_url: string;
check_suite_url: string;
artifacts_url: string;
cancel_url: string;
rerun_url: string;
workflow_url: string;
head_commit: HeadCommit;
repository: Repository;
head_repository: HeadRepository;
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',
}
@@ -1,194 +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,
Box,
LinearProgress,
makeStyles,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableRow,
Theme,
Typography,
} from '@material-ui/core';
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
import { useAsync } from 'react-use';
import { Link, useApi, githubAuthApiRef } from '@backstage/core';
import { githubActionsApiRef } from '../../api';
import { BuildSteps } from'../BuildSteps/BuildSteps';
import ArrowBackRoundedIcon from '@material-ui/icons/ArrowBackRounded';
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();
let jobs: {
jobName: string,
jobStart: string,
jobEnd: string,
jobStatus: string,
steps: any[]
}[] = [];
const [jobsArray] = useState(jobs);
const jobsStatus = useAsync(async () => {
const token = await auth.getAccessToken(['repo', 'user']);
return api
.listJobsForWorkflowRun({
token,
owner,
repo,
id: parseInt(id, 10),
})
.then(data => {
data.jobs.map(job => {
jobs.push({
jobName: job.name,
jobStart: job.started_at,
jobEnd: job.completed_at,
jobStatus: job.conclusion,
steps: job.steps
})
})
return data;
});
});
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="h1" color="primary">
<ArrowBackRoundedIcon />
</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>
<Typography className={classes.title} variant="h3">
Build Log
</Typography>
<Box>
{jobsStatus.loading
? <LinearProgress />
: jobsStatus.error
? <Typography variant="h6" color="error">
Failed to load build, {jobsStatus.error.message}
</Typography>
: jobsArray.map(
(job) => (
<BuildSteps runData={job} key={`${job.jobName}-${job.jobStart}`}/>
),
)
}
</Box>
</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>
);
};
@@ -1,74 +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 { IconComponent } from '@backstage/core';
import { makeStyles, Theme } 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;
};
type StatusStyle = {
icon: IconComponent;
color: string;
};
const styles: { [key in BuildStatus]: StatusStyle } = {
[BuildStatus.Null]: {
icon: UnknownIcon,
color: '#f49b20',
},
[BuildStatus.Success]: {
icon: SuccessIcon,
color: '#1db855',
},
[BuildStatus.Failure]: {
icon: FailureIcon,
color: '#CA001B',
},
[BuildStatus.Pending]: {
icon: UnknownIcon,
color: '#5BC0DE',
},
[BuildStatus.Running]: {
icon: ProgressIcon,
color: '#BEBEBE',
},
};
const useStyles = makeStyles<Theme, StatusStyle>({
icon: style => ({
color: style.color,
}),
});
export const BuildStatusIndicator = ({ status }: Props) => {
const style = (status && styles[status]) || styles[BuildStatus.Null];
const classes = useStyles(style);
const Icon = style.icon;
return (
<div className={classes.icon}>
<Icon />
</div>
);
};
@@ -0,0 +1,158 @@
/*
* 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 React, { useEffect } from 'react';
import { useWorkflowRuns } from '../useWorkflowRuns';
import { WorkflowRun, WorkflowRunsTable } from '../WorkflowRunsTable';
import { Entity } from '@backstage/catalog-model';
import { WorkflowRunStatus } from '../WorkflowRunStatus';
import {
Link,
Theme,
makeStyles,
LinearProgress,
Typography,
} from '@material-ui/core';
import {
InfoCard,
StructuredMetadataTable,
errorApiRef,
useApi,
} from '@backstage/core';
import ExternalLinkIcon from '@material-ui/icons/Launch';
const useStyles = makeStyles<Theme>({
externalLinkIcon: {
fontSize: 'inherit',
verticalAlign: 'bottom',
},
});
const WidgetContent = ({
error,
loading,
lastRun,
branch,
}: {
error?: Error;
loading?: boolean;
lastRun: WorkflowRun;
branch: string;
}) => {
const classes = useStyles();
if (error) return <Typography>Couldn't fetch latest {branch} run</Typography>;
if (loading) return <LinearProgress />;
return (
<StructuredMetadataTable
metadata={{
status: (
<>
<WorkflowRunStatus status={lastRun.status} />
</>
),
message: lastRun.message,
url: (
<Link href={lastRun.githubUrl} target="_blank">
See more on GitHub{' '}
<ExternalLinkIcon className={classes.externalLinkIcon} />
</Link>
),
}}
/>
);
};
export const Widget = ({
entity,
branch = 'master',
}: {
entity: Entity;
branch: string;
}) => {
const errorApi = useApi(errorApiRef);
const [owner, repo] = (
entity?.metadata.annotations?.['backstage.io/github-actions-id'] ?? '/'
).split('/');
const [{ runs, loading, error }] = useWorkflowRuns({
owner,
repo,
branch,
});
const lastRun = runs?.[0] ?? ({} as WorkflowRun);
useEffect(() => {
if (error) {
errorApi.post(error);
}
}, [error, errorApi]);
return (
<InfoCard title={`Last ${branch} build`}>
<WidgetContent
error={error}
loading={loading}
branch={branch}
lastRun={lastRun}
/>
</InfoCard>
);
};
const RecentWorkflowRunsCardContent = ({
error,
loading,
branch,
}: {
error?: Error;
loading?: boolean;
branch: string;
}) => {
if (error) return <Typography>Couldn't fetch {branch} runs</Typography>;
if (loading) return <LinearProgress />;
return <WorkflowRunsTable />;
};
export const RecentWorkflowRunsCard = ({
entity,
branch = 'master',
}: {
entity: Entity;
branch: string;
}) => {
const errorApi = useApi(errorApiRef);
const [owner, repo] = (
entity?.metadata.annotations?.['backstage.io/github-actions-id'] ?? '/'
).split('/');
const [{ loading, error }] = useWorkflowRuns({
owner,
repo,
branch,
});
useEffect(() => {
if (error) {
errorApi.post(error);
}
}, [error, errorApi]);
return (
<InfoCard title={`${branch} builds`}>
<RecentWorkflowRunsCardContent
error={error}
loading={loading}
branch={branch}
/>
</InfoCard>
);
};
@@ -13,5 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { BuildInfoCard } from './BuildInfoCard';
export { Widget, RecentWorkflowRunsCard } from './Widget';
@@ -0,0 +1,236 @@
/*
* 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 React from 'react';
import { useEntityCompoundName } from '@backstage/plugin-catalog';
import { useWorkflowRunsDetails } from './useWorkflowRunsDetails';
import { useWorkflowRunJobs } from './useWorkflowRunJobs';
import { useProjectName } from '../useProjectName';
import {
makeStyles,
Box,
TableRow,
TableCell,
ListItemText,
ExpansionPanel,
ExpansionPanelSummary,
Typography,
ExpansionPanelDetails,
TableContainer,
Table,
Paper,
TableBody,
LinearProgress,
CircularProgress,
Theme,
Link,
} from '@material-ui/core';
import { Jobs, Job, Step } from '../../api';
import moment from 'moment';
import { WorkflowRunStatus } from '../WorkflowRunStatus';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import ExternalLinkIcon from '@material-ui/icons/Launch';
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',
},
externalLinkIcon: {
fontSize: 'inherit',
verticalAlign: 'bottom',
},
}));
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 (
<TableRow>
<TableCell>
<ListItemText
primary={step.name}
secondary={getElapsedTime(step.started_at, step.completed_at)}
/>
</TableCell>
<TableCell>
<WorkflowRunStatus status={step.status.toUpperCase()} />
</TableCell>
</TableRow>
);
};
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}>
<TableContainer>
<Table>
{job.steps.map((step: Step) => (
<StepView step={step} />
))}
</Table>
</TableContainer>
</ExpansionPanelDetails>
</ExpansionPanel>
);
};
export const WorkflowRunDetails = () => {
let entityCompoundName = useEntityCompoundName();
if (!entityCompoundName.name) {
// TODO(shmidt-i): remove when is fully integrated
// into the entity view
entityCompoundName = {
kind: 'Component',
name: 'backstage',
namespace: 'default',
};
}
const projectName = useProjectName(entityCompoundName);
const [owner, repo] = projectName.value ? projectName.value.split('/') : [];
const details = useWorkflowRunsDetails(repo, owner);
const jobs = useWorkflowRunJobs(details.value?.jobs_url);
const error = projectName.error || (projectName.value && details.error);
const classes = useStyles();
if (error) {
return (
<Typography variant="h6" color="error">
Failed to load build, {error.message}
</Typography>
);
} else if (projectName.loading || details.loading) {
return <LinearProgress />;
}
return (
<div className={classes.root}>
<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>
<WorkflowRunStatus 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 && (
<Link target="_blank" href={details.value.html_url}>
Workflow runs on GitHub{' '}
<ExternalLinkIcon className={classes.externalLinkIcon} />
</Link>
)}
</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={2}>
<Typography noWrap>Jobs</Typography>
{jobs.loading ? (
<CircularProgress />
) : (
<JobsList jobs={jobs.value} />
)}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</div>
);
};
@@ -13,5 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { BuildStatusIndicator } from './BuildStatusIndicator';
export { WorkflowRunDetails } from './WorkflowRunDetails';
@@ -0,0 +1,32 @@
/*
* 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 { useAsync } from 'react-use';
import { Jobs } from '../../api/types';
export const useWorkflowRunJobs = (jobsUrl?: string) => {
const jobs = useAsync(async (): Promise<Jobs> => {
if (jobsUrl === undefined) {
return {
total_count: 0,
jobs: [],
};
}
const data = await fetch(jobsUrl).then(d => d.json());
return data;
}, [jobsUrl]);
return jobs;
};
@@ -0,0 +1,37 @@
/*
* 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']);
return repo && owner
? api.getWorkflowRun({
token,
owner,
repo,
id: parseInt(id, 10),
})
: Promise.reject('No repo/owner provided');
}, [repo, owner, id]);
return details;
};
@@ -0,0 +1,65 @@
/*
* 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 { Typography, Grid, Breadcrumbs } from '@material-ui/core';
import React from 'react';
import {
Link,
Page,
Header,
HeaderLabel,
Content,
ContentHeader,
SupportButton,
pageTheme,
} from '@backstage/core';
import { WorkflowRunDetails } from '../WorkflowRunDetails';
/**
* A component for Jobs visualization. Jobs are a property of a Workflow Run.
*/
export const WorkflowRunDetailsPage = () => {
return (
<Page theme={pageTheme.tool}>
<Header
title="GitHub Actions"
subtitle="See recent workflow runs and their status"
>
<HeaderLabel label="Owner" value="Spotify" />
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
<Content>
<ContentHeader title="Workflow run details">
<SupportButton>
This plugin allows you to view and interact with your builds within
the GitHub Actions environment.
</SupportButton>
</ContentHeader>
<Breadcrumbs aria-label="breadcrumb">
<Link to="/github-actions">Workflow runs</Link>
<Typography>Workflow run details</Typography>
</Breadcrumbs>
<Grid container spacing={3} direction="column">
<Grid item>
<WorkflowRunDetails />
</Grid>
</Grid>
</Content>
</Page>
);
};
@@ -0,0 +1,17 @@
/*
* 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 { WorkflowRunDetailsPage } from './WorkflowRunDetailsPage';
@@ -0,0 +1,52 @@
/*
* 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 { StatusPending, StatusRunning, StatusOK } from '@backstage/core';
import React from 'react';
export const WorkflowRunStatus = ({
status,
}: {
status: string | undefined;
}) => {
if (status === undefined) return null;
switch (status.toLowerCase()) {
case 'queued':
return (
<>
<StatusPending /> Queued
</>
);
case 'in_progress':
return (
<>
<StatusRunning /> In progress
</>
);
case 'completed':
return (
<>
<StatusOK /> Completed
</>
);
default:
return (
<>
<StatusPending /> Pending
</>
);
}
};
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { BuildDetailsPage } from './BuildDetailsPage';
export { WorkflowRunStatus } from './WorkflowRunStatus';
@@ -26,20 +26,20 @@ import {
import { Grid } from '@material-ui/core';
import React from 'react';
import { BuildListTable } from '../BuildListTable';
import { WorkflowRunsTable } from '../WorkflowRunsTable';
export const BuildListPage = () => {
export const WorkflowRunsPage = () => {
return (
<Page theme={pageTheme.tool}>
<Header
title="GitHub Actions"
subtitle="See recent builds and their status"
subtitle="See recent workflow 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 +47,7 @@ export const BuildListPage = () => {
</ContentHeader>
<Grid container spacing={3} direction="column">
<Grid item>
<BuildListTable repo="try-ssr" owner="CircleCITest3" />
<WorkflowRunsTable />
</Grid>
</Grid>
</Content>
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { BuildListPage } from './BuildListPage';
export { WorkflowRunsPage } from './WorkflowRunsPage';
@@ -14,25 +14,23 @@
* limitations under the License.
*/
import React, { FC } from 'react';
import { Link, Typography, Box, IconButton } from '@material-ui/core';
import { Link, Typography, Box, IconButton, Tooltip } from '@material-ui/core';
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 { Link as RouterLink, generatePath } from 'react-router-dom';
import { Table, TableColumn } from '@backstage/core';
import { useWorkflowRuns } from '../useWorkflowRuns';
import { WorkflowRunStatus } from '../WorkflowRunStatus';
import SyncIcon from '@material-ui/icons/Sync';
import { buildRouteRef } from '../../plugin';
import { useEntityCompoundName } from '@backstage/plugin-catalog';
import { useProjectName } from '../useProjectName';
export type Build = {
export type WorkflowRun = {
id: string;
buildName: string;
buildUrl?: string;
message: string;
url?: string;
githubUrl?: string;
source: {
branchName: string;
commit: {
@@ -41,25 +39,7 @@ export type Build = {
};
};
status: string;
onRestartClick: () => 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':
return <StatusRunning />;
case 'failed':
return <StatusError />;
case 'success':
return <StatusOK />;
case 'canceled':
default:
return <StatusWarning />;
}
onReRunClick: () => void;
};
const generatedColumns: TableColumn[] = [
@@ -70,40 +50,45 @@ 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={generatePath(buildRouteRef.path, { id: row.id! })}
>
{row.message}
</Link>
),
},
{
title: 'Source',
render: (row: Partial<Build>) => (
<>
render: (row: Partial<WorkflowRun>) => (
<Typography variant="body2" noWrap>
<p>{row.source?.branchName}</p>
<p>{row.source?.commit.hash}</p>
</>
</Typography>
),
},
{
title: 'Status',
render: (row: Partial<Build>) => (
width: '150px',
render: (row: Partial<WorkflowRun>) => (
<Box display="flex" alignItems="center">
{getStatusComponent(row.status)}
<Box mr={1} />
<Typography variant="button">{row.status}</Typography>
<WorkflowRunStatus status={row.status} />
</Box>
),
},
{
title: 'Actions',
render: (row: Partial<Build>) => (
<IconButton onClick={row.onRestartClick}>
<RetryIcon />
</IconButton>
render: (row: Partial<WorkflowRun>) => (
<Tooltip title="Rerun workflow">
<IconButton onClick={row.onReRunClick}>
<RetryIcon />
</IconButton>
</Tooltip>
),
width: '10%',
},
@@ -112,7 +97,7 @@ const generatedColumns: TableColumn[] = [
type Props = {
loading: boolean;
retry: () => void;
builds?: Build[];
runs?: WorkflowRun[];
projectName: string;
page: number;
onChangePage: (page: number) => void;
@@ -121,13 +106,13 @@ type Props = {
onChangePageSize: (pageSize: number) => void;
};
const BuildListTableView: FC<Props> = ({
export const WorkflowRunsTableView: FC<Props> = ({
projectName,
loading,
pageSize,
page,
retry,
builds,
runs,
onChangePage,
onChangePageSize,
total,
@@ -140,13 +125,13 @@ const BuildListTableView: FC<Props> = ({
page={page}
actions={[
{
icon: () => <RetryIcon />,
tooltip: 'Refresh Data',
icon: () => <SyncIcon />,
tooltip: 'Reload workflow runs',
isFreeAction: true,
onClick: () => retry(),
},
]}
data={builds ?? []}
data={runs ?? []}
onChangePage={onChangePage}
onChangeRowsPerPage={onChangePageSize}
title={
@@ -161,20 +146,29 @@ const BuildListTableView: FC<Props> = ({
);
};
export const BuildListTable = ({
repo,
owner,
}: {
repo: string;
owner: string;
}) => {
const [tableProps, { retry, setPage, setPageSize }] = useBuilds({
repo,
export const WorkflowRunsTable = () => {
let entityCompoundName = useEntityCompoundName();
if (!entityCompoundName.name) {
// TODO(shmidt-i): remove when is fully integrated
// into the entity view
entityCompoundName = {
kind: 'Component',
name: 'backstage',
namespace: 'default',
};
}
const { value: projectName, loading } = useProjectName(entityCompoundName);
const [owner, repo] = (projectName ?? '/').split('/');
const [tableProps, { retry, setPage, setPageSize }] = useWorkflowRuns({
owner,
repo,
});
return (
<BuildListTableView
<WorkflowRunsTableView
{...tableProps}
loading={loading || tableProps.loading}
retry={retry}
onChangePageSize={setPageSize}
onChangePage={setPage}
@@ -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, WorkflowRunsTableView } from './WorkflowRunsTable';
export type { WorkflowRun } from './WorkflowRunsTable';
@@ -0,0 +1,29 @@
/*
* 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 { useAsync } from 'react-use';
import { catalogApiRef, EntityCompoundName } from '@backstage/plugin-catalog';
import { useApi } from '@backstage/core';
export const useProjectName = (name: EntityCompoundName) => {
const catalogApi = useApi(catalogApiRef);
const { value, loading, error } = useAsync(async () => {
const entity = await catalogApi.getEntityByName(name);
return entity?.metadata.annotations?.['github.com/project-slug'] ?? '';
});
return { value, loading, error };
};
@@ -15,42 +15,64 @@
*/
import { useState } from 'react';
import { useAsyncRetry } from 'react-use';
import { Build } from './BuildListTable';
import { githubActionsApiRef } from '../../api/GithubActionsApi';
import { useApi, githubAuthApiRef } from '@backstage/core';
import { WorkflowRun } from './WorkflowRunsTable/WorkflowRunsTable';
import { githubActionsApiRef } from '../api/GithubActionsApi';
import { useApi, githubAuthApiRef, errorApiRef } from '@backstage/core';
import { ActionsListWorkflowRunsForRepoResponseData } from '@octokit/types';
export function useBuilds({ repo, owner }: { repo: string; owner: string }) {
export function useWorkflowRuns({
owner,
repo,
branch,
}: {
owner: string;
repo: string;
branch?: 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 { loading, value: builds, retry } = useAsyncRetry<Build[]>(async () => {
const token = await auth.getAccessToken(['repo', 'user']);
const { loading, value: runs, retry, error } = useAsyncRetry<
WorkflowRun[]
>(async () => {
const token = await auth.getAccessToken(['repo']);
return (
api
// GitHub API pagination count starts from 1
.listWorkflowRuns({ token, owner, repo, pageSize, page: page + 1 })
.listWorkflowRuns({
token,
owner,
repo,
pageSize,
page: page + 1,
branch,
})
.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,28 +85,28 @@ export function useBuilds({ repo, owner }: { repo: string; owner: string }) {
},
},
status: run.status,
buildUrl: run.url,
url: run.url,
githubUrl: run.html_url,
}));
},
)
);
}, [page, pageSize]);
}, [page, pageSize, repo, owner]);
const projectName = `${owner}/${repo}`;
return [
{
page,
pageSize,
loading,
builds,
projectName,
runs,
projectName: `${owner}/${repo}`,
total,
error,
},
{
builds,
runs,
setPage,
setPageSize,
restartBuild,
retry,
},
] as const;
+1
View File
@@ -16,3 +16,4 @@
export { plugin } from './plugin';
export * from './api';
export { Widget } from './components/Widget';
+11 -6
View File
@@ -15,23 +15,28 @@
*/
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({
path: '/github-actions',
title: 'GitHub Actions',
});
export const projectRouteRef = createRouteRef({
path: '/github-actions/:kind/:optionalNamespaceAndName/',
title: 'GitHub Actions for project',
});
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(projectRouteRef, WorkflowRunsPage);
router.addRoute(buildRouteRef, WorkflowRunDetailsPage);
},
});