From 21bd252051de1a42bd396db07bc297360a4db687 Mon Sep 17 00:00:00 2001 From: NetPenguins Date: Sat, 29 Aug 2020 09:59:38 -0400 Subject: [PATCH] Added LazyLog to Github-Actions for displaying workflow run logs. --- .../src/api/GithubActionsApi.ts | 12 ++ .../src/api/GithubActionsClient.ts | 19 ++ .../BuildDetailsPage/BuildDetailsPage.tsx | 61 +++++- .../src/components/BuildSteps/BuildSteps.tsx | 182 ++++++++++++++++++ .../src/components/BuildSteps/index.ts | 16 ++ 5 files changed, 285 insertions(+), 5 deletions(-) create mode 100644 plugins/github-actions/src/components/BuildSteps/BuildSteps.tsx create mode 100644 plugins/github-actions/src/components/BuildSteps/index.ts diff --git a/plugins/github-actions/src/api/GithubActionsApi.ts b/plugins/github-actions/src/api/GithubActionsApi.ts index eedf9686f3..74c40c6916 100644 --- a/plugins/github-actions/src/api/GithubActionsApi.ts +++ b/plugins/github-actions/src/api/GithubActionsApi.ts @@ -19,6 +19,7 @@ import { ActionsListWorkflowRunsForRepoResponseData, ActionsGetWorkflowResponseData, ActionsGetWorkflowRunResponseData, + ActionsListJobsForWorkflowRunResponseData } from '@octokit/types'; export const githubActionsApiRef = createApiRef({ @@ -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; }; diff --git a/plugins/github-actions/src/api/GithubActionsClient.ts b/plugins/github-actions/src/api/GithubActionsClient.ts index 1606c268f4..5c0594bdd3 100644 --- a/plugins/github-actions/src/api/GithubActionsClient.ts +++ b/plugins/github-actions/src/api/GithubActionsClient.ts @@ -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 { + const workflow = await new Octokit({ auth: token }).actions.listJobsForWorkflowRun({ + owner, + repo, + run_id: id, + }); + return workflow.data; + } } diff --git a/plugins/github-actions/src/components/BuildDetailsPage/BuildDetailsPage.tsx b/plugins/github-actions/src/components/BuildDetailsPage/BuildDetailsPage.tsx index 55c568106c..9a3cb615f6 100644 --- a/plugins/github-actions/src/components/BuildDetailsPage/BuildDetailsPage.tsx +++ b/plugins/github-actions/src/components/BuildDetailsPage/BuildDetailsPage.tsx @@ -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 => ({ root: { maxWidth: 720, @@ -46,14 +48,46 @@ const useStyles = makeStyles(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 = () => {
- - < + + Build Details @@ -138,6 +172,23 @@ export const BuildDetailsPage = () => { + + Build Log + + + {jobsStatus.loading + ? + : jobsStatus.error + ? + Failed to load build, {jobsStatus.error.message} + + : jobsArray.map( + (job) => ( + + ), + ) + } +
); }; diff --git a/plugins/github-actions/src/components/BuildSteps/BuildSteps.tsx b/plugins/github-actions/src/components/BuildSteps/BuildSteps.tsx new file mode 100644 index 0000000000..d6b117246a --- /dev/null +++ b/plugins/github-actions/src/components/BuildSteps/BuildSteps.tsx @@ -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, + 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 + }; +}> = ({ 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 ( + + } + aria-controls={`panel-${runData.jobName}-content`} + id={`panel-${runData.jobName}-header`} + > + { + runData.jobStatus == 'success' ? + + : + } + + {runData.jobName || "No Run Logs"} {`(${timeElapsed(runData.jobStart, runData.jobEnd, true)})` || ""} + + + + {messages.length === 0 ? ( + 'Nothing here...' + ) : ( + }> +
+ { + if(line.toLocaleLowerCase().includes("error") + || line.toLocaleLowerCase().includes("failed") + || line.toLocaleLowerCase().includes("failure")) + { + return + } + return line; + }} + /> +
+
+ )} +
+
+ ); +}; \ No newline at end of file diff --git a/plugins/github-actions/src/components/BuildSteps/index.ts b/plugins/github-actions/src/components/BuildSteps/index.ts new file mode 100644 index 0000000000..ef173b9895 --- /dev/null +++ b/plugins/github-actions/src/components/BuildSteps/index.ts @@ -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';