Merge pull request #1762 from spotify/shmidt-i/github-actions-widget-redone
feat(github-actions): widget inside catalog
This commit is contained in:
@@ -33,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,
|
||||
|
||||
@@ -46,12 +46,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,
|
||||
@@ -60,6 +62,7 @@ export class GithubActionsClient implements GithubActionsApi {
|
||||
repo,
|
||||
per_page: pageSize,
|
||||
page,
|
||||
...(branch ? { branch } : {}),
|
||||
});
|
||||
return workflowRuns.data;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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 } 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 { Widget } 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 { WorkflowRunDetails } from './WorkflowRunDetails';
|
||||
+8
-6
@@ -24,12 +24,14 @@ export const useWorkflowRunsDetails = (repo: string, owner: string) => {
|
||||
const { id } = useParams();
|
||||
const details = useAsync(async () => {
|
||||
const token = await auth.getAccessToken(['repo']);
|
||||
return api.getWorkflowRun({
|
||||
token,
|
||||
owner,
|
||||
repo,
|
||||
id: parseInt(id, 10),
|
||||
});
|
||||
return repo && owner
|
||||
? api.getWorkflowRun({
|
||||
token,
|
||||
owner,
|
||||
repo,
|
||||
id: parseInt(id, 10),
|
||||
})
|
||||
: Promise.reject('No repo/owner provided');
|
||||
}, [repo, owner, id]);
|
||||
return details;
|
||||
};
|
||||
+3
-214
@@ -14,28 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Button,
|
||||
LinearProgress,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
Theme,
|
||||
Typography,
|
||||
Box,
|
||||
ExpansionPanelDetails,
|
||||
ExpansionPanel,
|
||||
ExpansionPanelSummary,
|
||||
ListItemText,
|
||||
CircularProgress,
|
||||
Grid,
|
||||
Breadcrumbs,
|
||||
} from '@material-ui/core';
|
||||
import moment from 'moment';
|
||||
import { Typography, Grid, Breadcrumbs } from '@material-ui/core';
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
@@ -48,133 +27,13 @@ import {
|
||||
SupportButton,
|
||||
pageTheme,
|
||||
} from '@backstage/core';
|
||||
import { Job, Step, Jobs } from '../../api/types';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { useWorkflowRunsDetails } from './useWorkflowRunsDetails';
|
||||
import { useWorkflowRunJobs } from './useWorkflowRunJobs';
|
||||
import { WorkflowRunStatusIcon } from '../WorkflowRunStatusIcon/WorkflowRunStatusIcon';
|
||||
import { useProjectName } from '../useProjectName';
|
||||
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||
|
||||
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 (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<ListItemText
|
||||
primary={step.name}
|
||||
secondary={getElapsedTime(step.started_at, step.completed_at)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<WorkflowRunStatusIcon status={step.status.toUpperCase()} />
|
||||
{step.status}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
import { WorkflowRunDetails } from '../WorkflowRunDetails';
|
||||
|
||||
/**
|
||||
* A component for Jobs visualization. Jobs are a property of a Workflow Run.
|
||||
*/
|
||||
export const WorkflowRunDetailsPage = () => {
|
||||
const [owner, repo] = (
|
||||
useProjectName({
|
||||
kind: 'Component',
|
||||
name: 'backstage',
|
||||
}) ?? '/'
|
||||
).split('/');
|
||||
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 (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
@@ -197,77 +56,7 @@ export const WorkflowRunDetailsPage = () => {
|
||||
</Breadcrumbs>
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<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>
|
||||
<WorkflowRunStatusIcon status={details.value?.status} />{' '}
|
||||
{details.value?.status.toUpperCase()}
|
||||
</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 && (
|
||||
<a href={details.value.html_url}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="default"
|
||||
startIcon={<GitHubIcon />}
|
||||
>
|
||||
Workflow runs on GitHub
|
||||
</Button>
|
||||
</a>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={2}>
|
||||
<Typography noWrap>Jobs</Typography>
|
||||
{jobs.loading ? (
|
||||
<CircularProgress />
|
||||
) : (
|
||||
<JobsList jobs={jobs.value} />
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
<WorkflowRunDetails />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
|
||||
+21
-5
@@ -17,7 +17,7 @@
|
||||
import { StatusPending, StatusRunning, StatusOK } from '@backstage/core';
|
||||
import React from 'react';
|
||||
|
||||
export const WorkflowRunStatusIcon = ({
|
||||
export const WorkflowRunStatus = ({
|
||||
status,
|
||||
}: {
|
||||
status: string | undefined;
|
||||
@@ -25,12 +25,28 @@ export const WorkflowRunStatusIcon = ({
|
||||
if (status === undefined) return null;
|
||||
switch (status.toLowerCase()) {
|
||||
case 'queued':
|
||||
return <StatusPending />;
|
||||
return (
|
||||
<>
|
||||
<StatusPending /> Queued
|
||||
</>
|
||||
);
|
||||
case 'in_progress':
|
||||
return <StatusRunning />;
|
||||
return (
|
||||
<>
|
||||
<StatusRunning /> In progress
|
||||
</>
|
||||
);
|
||||
case 'completed':
|
||||
return <StatusOK />;
|
||||
return (
|
||||
<>
|
||||
<StatusOK /> Completed
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return <StatusPending />;
|
||||
return (
|
||||
<>
|
||||
<StatusPending /> Pending
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { WorkflowRunStatusIcon } from './WorkflowRunStatusIcon';
|
||||
export { WorkflowRunStatus } from './WorkflowRunStatus';
|
||||
@@ -17,16 +17,20 @@ import React, { FC } from 'react';
|
||||
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 { Link as RouterLink, generatePath } from 'react-router-dom';
|
||||
import { Table, TableColumn } from '@backstage/core';
|
||||
import { useWorkflowRuns } from './useWorkflowRuns';
|
||||
import { WorkflowRunStatusIcon } from '../WorkflowRunStatusIcon';
|
||||
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 WorkflowRun = {
|
||||
id: string;
|
||||
message: string;
|
||||
url?: string;
|
||||
githubUrl?: string;
|
||||
source: {
|
||||
branchName: string;
|
||||
commit: {
|
||||
@@ -52,7 +56,7 @@ const generatedColumns: TableColumn[] = [
|
||||
render: (row: Partial<WorkflowRun>) => (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`/github-actions/workflow-run/${row.id}`}
|
||||
to={generatePath(buildRouteRef.path, { id: row.id! })}
|
||||
>
|
||||
{row.message}
|
||||
</Link>
|
||||
@@ -69,13 +73,11 @@ const generatedColumns: TableColumn[] = [
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
width: '150px',
|
||||
|
||||
render: (row: Partial<WorkflowRun>) => (
|
||||
<Box display="flex" alignItems="center">
|
||||
<WorkflowRunStatusIcon status={row.status} />
|
||||
<Box mr={1} />
|
||||
<Typography variant="button" noWrap>
|
||||
{row.status}
|
||||
</Typography>
|
||||
<WorkflowRunStatus status={row.status} />
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
@@ -104,7 +106,7 @@ type Props = {
|
||||
onChangePageSize: (pageSize: number) => void;
|
||||
};
|
||||
|
||||
const WorkflowRunsTableView: FC<Props> = ({
|
||||
export const WorkflowRunsTableView: FC<Props> = ({
|
||||
projectName,
|
||||
loading,
|
||||
pageSize,
|
||||
@@ -145,10 +147,28 @@ const WorkflowRunsTableView: FC<Props> = ({
|
||||
};
|
||||
|
||||
export const WorkflowRunsTable = () => {
|
||||
const [tableProps, { retry, setPage, setPageSize }] = useWorkflowRuns();
|
||||
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 (
|
||||
<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 { WorkflowRunsTable } from './WorkflowRunsTable';
|
||||
export { WorkflowRunsTable, WorkflowRunsTableView } from './WorkflowRunsTable';
|
||||
export type { WorkflowRun } from './WorkflowRunsTable';
|
||||
|
||||
@@ -21,9 +21,9 @@ import { useApi } from '@backstage/core';
|
||||
export const useProjectName = (name: EntityCompoundName) => {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
|
||||
const { value } = useAsync<string>(async () => {
|
||||
const { value, loading, error } = useAsync<string>(async () => {
|
||||
const entity = await catalogApi.getEntityByName(name);
|
||||
return entity?.metadata.annotations?.['github.com/project-slug'] ?? '';
|
||||
});
|
||||
return value;
|
||||
return { value, loading, error };
|
||||
};
|
||||
|
||||
+24
-13
@@ -15,13 +15,20 @@
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useAsyncRetry } from 'react-use';
|
||||
import { WorkflowRun } from './WorkflowRunsTable';
|
||||
import { githubActionsApiRef } from '../../api/GithubActionsApi';
|
||||
import { WorkflowRun } from './WorkflowRunsTable/WorkflowRunsTable';
|
||||
import { githubActionsApiRef } from '../api/GithubActionsApi';
|
||||
import { useApi, githubAuthApiRef, errorApiRef } from '@backstage/core';
|
||||
import { ActionsListWorkflowRunsForRepoResponseData } from '@octokit/types';
|
||||
import { useProjectName } from '../useProjectName';
|
||||
|
||||
export function useWorkflowRuns() {
|
||||
export function useWorkflowRuns({
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
}: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
branch?: string;
|
||||
}) {
|
||||
const api = useApi(githubActionsApiRef);
|
||||
const auth = useApi(githubAuthApiRef);
|
||||
|
||||
@@ -31,19 +38,21 @@ export function useWorkflowRuns() {
|
||||
const [page, setPage] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
|
||||
const projectName = useProjectName({
|
||||
kind: 'Component',
|
||||
name: 'backstage',
|
||||
});
|
||||
const { loading, value: runs, retry } = useAsyncRetry<
|
||||
const { loading, value: runs, retry, error } = useAsyncRetry<
|
||||
WorkflowRun[]
|
||||
>(async () => {
|
||||
const token = await auth.getAccessToken(['repo']);
|
||||
const [owner, repo] = (projectName ?? '/').split('/');
|
||||
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(
|
||||
(
|
||||
workflowRunsData: ActionsListWorkflowRunsForRepoResponseData,
|
||||
@@ -77,11 +86,12 @@ export function useWorkflowRuns() {
|
||||
},
|
||||
status: run.status,
|
||||
url: run.url,
|
||||
githubUrl: run.html_url,
|
||||
}));
|
||||
},
|
||||
)
|
||||
);
|
||||
}, [page, pageSize, projectName]);
|
||||
}, [page, pageSize, repo, owner]);
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -89,8 +99,9 @@ export function useWorkflowRuns() {
|
||||
pageSize,
|
||||
loading,
|
||||
runs,
|
||||
projectName: projectName ?? '',
|
||||
projectName: `${owner}/${repo}`,
|
||||
total,
|
||||
error,
|
||||
},
|
||||
{
|
||||
runs,
|
||||
@@ -16,3 +16,4 @@
|
||||
|
||||
export { plugin } from './plugin';
|
||||
export * from './api';
|
||||
export { Widget } from './components/Widget';
|
||||
|
||||
Reference in New Issue
Block a user