Update to include JobLog component.
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
"moment": "^2.27.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-lazylog": "^4.5.3",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ import { createApiRef } from '@backstage/core';
|
||||
import {
|
||||
ActionsListWorkflowRunsForRepoResponseData,
|
||||
ActionsGetWorkflowResponseData,
|
||||
ActionsGetWorkflowRunResponseData
|
||||
ActionsGetWorkflowRunResponseData,
|
||||
} from '@octokit/types';
|
||||
|
||||
export const githubActionsApiRef = createApiRef<GithubActionsApi>({
|
||||
@@ -75,4 +75,15 @@ export type GithubActionsApi = {
|
||||
repo: string;
|
||||
runId: number;
|
||||
}) => Promise<any>;
|
||||
downloadJobLogsForWorkflowRun: ({
|
||||
token,
|
||||
owner,
|
||||
repo,
|
||||
runId,
|
||||
}: {
|
||||
token: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
runId: number;
|
||||
}) => Promise<any>;
|
||||
};
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
ActionsListWorkflowRunsForRepoResponseData,
|
||||
ActionsGetWorkflowResponseData,
|
||||
ActionsGetWorkflowRunResponseData,
|
||||
ActionsListJobsForWorkflowRunResponseData
|
||||
} from '@octokit/types';
|
||||
|
||||
export class GithubActionsClient implements GithubActionsApi {
|
||||
@@ -103,22 +102,22 @@ export class GithubActionsClient implements GithubActionsApi {
|
||||
});
|
||||
return run.data;
|
||||
}
|
||||
async listJobsForWorkflowRun({
|
||||
async downloadJobLogsForWorkflowRun({
|
||||
token,
|
||||
owner,
|
||||
repo,
|
||||
id,
|
||||
runId,
|
||||
}: {
|
||||
token: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
id: number;
|
||||
}): Promise<ActionsListJobsForWorkflowRunResponseData> {
|
||||
const workflow = await new Octokit({ auth: token }).actions.listJobsForWorkflowRun({
|
||||
runId: number;
|
||||
}): Promise<any> {
|
||||
const workflow = await new Octokit({ auth: token }).actions.downloadJobLogsForWorkflowRun({
|
||||
owner,
|
||||
repo,
|
||||
run_id: id,
|
||||
job_id: runId,
|
||||
});
|
||||
return workflow.data;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ export type Job = {
|
||||
conclusion: string;
|
||||
started_at: string;
|
||||
completed_at: string;
|
||||
id: string;
|
||||
name: string;
|
||||
steps: Step[];
|
||||
};
|
||||
|
||||
@@ -1,181 +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 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){
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -18,6 +18,8 @@ import { useEntityCompoundName } from '@backstage/plugin-catalog';
|
||||
import { useWorkflowRunsDetails } from './useWorkflowRunsDetails';
|
||||
import { useWorkflowRunJobs } from './useWorkflowRunJobs';
|
||||
import { useProjectName } from '../useProjectName';
|
||||
import { WorkflowRunLogs } from '../WorkflowRunLogs';
|
||||
|
||||
import {
|
||||
makeStyles,
|
||||
Box,
|
||||
@@ -136,6 +138,9 @@ const JobListItem = ({ job, className }: { job: Job; className: string }) => {
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</ExpansionPanelDetails>
|
||||
<WorkflowRunLogs
|
||||
runId={job.id}
|
||||
/>
|
||||
</ExpansionPanel>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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,
|
||||
ExpansionPanel,
|
||||
Typography,
|
||||
makeStyles,
|
||||
Theme,
|
||||
ExpansionPanelSummary,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import React, {Suspense} from 'react';
|
||||
import { useEntityCompoundName } from '@backstage/plugin-catalog';
|
||||
import { useDownloadWorkflowRunLogs } from './useDownloadWorkflowRunLogs';
|
||||
import LinePart from 'react-lazylog/build/LinePart';
|
||||
import { useProjectName } from '../useProjectName';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
|
||||
const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog'));
|
||||
|
||||
const useStyles = makeStyles<Theme>(() => ({
|
||||
button: {
|
||||
order: -1,
|
||||
marginRight: 0,
|
||||
marginLeft: '-20px',
|
||||
},
|
||||
}));
|
||||
|
||||
const DisplayLog = (jobLogs: any) =>{
|
||||
return(
|
||||
<Suspense fallback={<LinearProgress />}>
|
||||
<div style={{ height: '50vh', width: '100%' }}>
|
||||
<LazyLog
|
||||
text={jobLogs.jobLogs ?? "No Values Found"}
|
||||
extraLines={1}
|
||||
caseInsensitive
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A component for Run Logs visualization.
|
||||
*/
|
||||
export const WorkflowRunLogs = ({runId}:{runId: string}) => {
|
||||
const classes = useStyles();
|
||||
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 jobLogs = useDownloadWorkflowRunLogs(repo, owner, runId);
|
||||
|
||||
return (
|
||||
<ExpansionPanel
|
||||
TransitionProps={{ unmountOnExit: true }}
|
||||
>
|
||||
<ExpansionPanelSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls={`panel-${name}-content`}
|
||||
id={`panel-${name}-header`}
|
||||
IconButtonProps={{
|
||||
className: classes.button,
|
||||
}}
|
||||
>
|
||||
<Typography variant="button">
|
||||
{jobLogs.value === null ? "No Logs to Display" : "Job Log"}
|
||||
</Typography>
|
||||
</ExpansionPanelSummary>
|
||||
{jobLogs.value && <DisplayLog jobLogs={jobLogs.value || undefined}/>}
|
||||
</ExpansionPanel>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -13,4 +13,4 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { BuildSteps } from './BuildSteps';
|
||||
export { WorkflowRunLogs } from './WorkflowRunLogs';
|
||||
@@ -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 { useAsync } from 'react-use';
|
||||
import { githubActionsApiRef } from '../../api';
|
||||
|
||||
export const useDownloadWorkflowRunLogs = (repo: string, owner: string, id: string) => {
|
||||
const api = useApi(githubActionsApiRef);
|
||||
const auth = useApi(githubAuthApiRef);
|
||||
const details = useAsync(async () => {
|
||||
const token = await auth.getAccessToken(['repo']);
|
||||
return repo && owner
|
||||
? api.downloadJobLogsForWorkflowRun({
|
||||
token,
|
||||
owner,
|
||||
repo,
|
||||
runId: parseInt(id, 10),
|
||||
})
|
||||
: Promise.reject('No repo/owner provided');
|
||||
}, [repo, owner, id]);
|
||||
return details;
|
||||
};
|
||||
Reference in New Issue
Block a user