Added LazyLog to Github-Actions for displaying workflow run logs.
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
ActionsListWorkflowRunsForRepoResponseData,
|
||||
ActionsGetWorkflowResponseData,
|
||||
ActionsGetWorkflowRunResponseData,
|
||||
ActionsListJobsForWorkflowRunResponseData
|
||||
} from '@octokit/types';
|
||||
|
||||
export const githubActionsApiRef = createApiRef<GithubActionsApi>({
|
||||
@@ -73,4 +74,15 @@ export type GithubActionsApi = {
|
||||
repo: string;
|
||||
runId: number;
|
||||
}) => void;
|
||||
listJobsForWorkflowRun: ({
|
||||
token,
|
||||
owner,
|
||||
repo,
|
||||
id,
|
||||
}: {
|
||||
token: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
id: number;
|
||||
}) => Promise<ActionsListJobsForWorkflowRunResponseData>;
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
ActionsListWorkflowRunsForRepoResponseData,
|
||||
ActionsGetWorkflowResponseData,
|
||||
ActionsGetWorkflowRunResponseData,
|
||||
ActionsListJobsForWorkflowRunResponseData
|
||||
} from '@octokit/types';
|
||||
|
||||
export class GithubActionsClient implements GithubActionsApi {
|
||||
@@ -99,4 +100,22 @@ export class GithubActionsClient implements GithubActionsApi {
|
||||
});
|
||||
return run.data;
|
||||
}
|
||||
async listJobsForWorkflowRun({
|
||||
token,
|
||||
owner,
|
||||
repo,
|
||||
id,
|
||||
}: {
|
||||
token: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
id: number;
|
||||
}): Promise<ActionsListJobsForWorkflowRunResponseData> {
|
||||
const workflow = await new Octokit({ auth: token }).actions.listJobsForWorkflowRun({
|
||||
owner,
|
||||
repo,
|
||||
run_id: id,
|
||||
});
|
||||
return workflow.data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import {
|
||||
Button,
|
||||
Box,
|
||||
LinearProgress,
|
||||
makeStyles,
|
||||
Paper,
|
||||
@@ -27,12 +28,13 @@ import {
|
||||
Theme,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import React from 'react';
|
||||
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,
|
||||
@@ -46,14 +48,46 @@ const useStyles = makeStyles<Theme>(theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
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
|
||||
@@ -84,8 +118,8 @@ export const BuildDetailsPage = () => {
|
||||
<div className={classes.root}>
|
||||
<Typography className={classes.title} variant="h3">
|
||||
<Link to="/github-actions">
|
||||
<Typography component="span" variant="h3" color="primary">
|
||||
<
|
||||
<Typography component="span" variant="h1" color="primary">
|
||||
<ArrowBackRoundedIcon />
|
||||
</Typography>
|
||||
</Link>
|
||||
Build Details
|
||||
@@ -138,6 +172,23 @@ export const BuildDetailsPage = () => {
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* 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, { useState, FC, Suspense, useEffect } from 'react';
|
||||
import {
|
||||
ExpansionPanel,
|
||||
ExpansionPanelSummary,
|
||||
Typography,
|
||||
ExpansionPanelDetails,
|
||||
LinearProgress,
|
||||
} from '@material-ui/core';
|
||||
import moment from 'moment';
|
||||
import CheckCircleRoundedIcon from '@material-ui/icons/CheckCircleRounded';
|
||||
import ErrorRoundedIcon from '@material-ui/icons/ErrorRounded';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import LinePart from 'react-lazylog/build/LinePart';
|
||||
const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog'));
|
||||
moment.relativeTimeThreshold('ss', 0);
|
||||
const useStyles = makeStyles(theme => ({
|
||||
expansionPanelDetails: {
|
||||
padding: 0,
|
||||
},
|
||||
button: {
|
||||
order: -1,
|
||||
marginRight: 0,
|
||||
marginLeft: '-20px',
|
||||
},
|
||||
neutral: {},
|
||||
failed: {
|
||||
position: 'relative',
|
||||
'&:after': {
|
||||
pointerEvents: 'none',
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
boxShadow: `inset 4px 0px 0px ${theme.palette.error.main}`,
|
||||
},
|
||||
},
|
||||
running: {
|
||||
position: 'relative',
|
||||
'&:after': {
|
||||
pointerEvents: 'none',
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
boxShadow: `inset 4px 0px 0px ${theme.palette.info.main}`,
|
||||
},
|
||||
},
|
||||
cardContent: {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
success: {
|
||||
position: 'relative',
|
||||
'&:after': {
|
||||
pointerEvents: 'none',
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
color: 'green',
|
||||
boxShadow: `inset 4px 0px 0px ${theme.palette.success.main}`,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const pickClassName = (
|
||||
classes: ReturnType<typeof useStyles>,
|
||||
build: any,
|
||||
) => {
|
||||
if (build === 'failed') return classes.failed;
|
||||
if (['running', 'queued'].includes(build!)) return classes.running;
|
||||
if (build === 'success') return classes.success;
|
||||
return classes.neutral;
|
||||
};
|
||||
|
||||
const timeElapsed = (
|
||||
start: string,
|
||||
end: string,
|
||||
humanize: boolean = false
|
||||
) => {
|
||||
if(humanize){
|
||||
return moment.duration(
|
||||
moment(start|| moment()).diff(moment(end)),
|
||||
).humanize();
|
||||
}else{
|
||||
return moment.duration(
|
||||
moment(start|| moment()).diff(moment(end)),
|
||||
).asSeconds();
|
||||
}
|
||||
}
|
||||
|
||||
export const BuildSteps: FC<{
|
||||
runData: {
|
||||
jobName: string,
|
||||
jobStart: string,
|
||||
jobEnd: string,
|
||||
jobStatus: string,
|
||||
steps: Array<any>
|
||||
};
|
||||
}> = ({ runData }) => {
|
||||
const classes = useStyles();
|
||||
const [messages, setMessages] = useState([] as any);
|
||||
|
||||
useEffect(() => {
|
||||
if(runData.steps !== undefined){
|
||||
console.log(runData.steps)
|
||||
setMessages(runData.steps.map(
|
||||
({ name, completed_at, started_at }: {
|
||||
name: string; completed_at: string; started_at: string;
|
||||
}) => `${name} ${timeElapsed(started_at, completed_at)}s`
|
||||
));
|
||||
}
|
||||
}, [runData.steps])
|
||||
|
||||
return (
|
||||
<ExpansionPanel
|
||||
TransitionProps={{ unmountOnExit: true }}
|
||||
className={pickClassName(classes, runData.jobStatus)}
|
||||
>
|
||||
<ExpansionPanelSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls={`panel-${runData.jobName}-content`}
|
||||
id={`panel-${runData.jobName}-header`}
|
||||
>
|
||||
{
|
||||
runData.jobStatus == 'success' ?
|
||||
<CheckCircleRoundedIcon style={{ color: 'lime' }}/>
|
||||
: <ErrorRoundedIcon style={{ color: 'red' }}/>
|
||||
}
|
||||
<Typography variant="button">
|
||||
{runData.jobName || "No Run Logs"} {`(${timeElapsed(runData.jobStart, runData.jobEnd, true)})` || ""}
|
||||
</Typography>
|
||||
</ExpansionPanelSummary>
|
||||
<ExpansionPanelDetails className={classes.expansionPanelDetails}>
|
||||
{messages.length === 0 ? (
|
||||
'Nothing here...'
|
||||
) : (
|
||||
<Suspense fallback={<LinearProgress />}>
|
||||
<div style={{ height: '25vh', width: '100%' }}>
|
||||
<LazyLog
|
||||
text={messages.join('\n')}
|
||||
extraLines={1}
|
||||
caseInsensitive={true}
|
||||
enableSearch
|
||||
formatPart={(line) => {
|
||||
if(line.toLocaleLowerCase().includes("error")
|
||||
|| line.toLocaleLowerCase().includes("failed")
|
||||
|| line.toLocaleLowerCase().includes("failure"))
|
||||
{
|
||||
return <LinePart style={{color: 'red'}} part={{text: line}}/>
|
||||
}
|
||||
return line;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Suspense>
|
||||
)}
|
||||
</ExpansionPanelDetails>
|
||||
</ExpansionPanel>
|
||||
);
|
||||
};
|
||||
@@ -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 { BuildSteps } from './BuildSteps';
|
||||
Reference in New Issue
Block a user