Merge pull request #2172 from NetPenguins/master

GithubActions Logging Implementation
This commit is contained in:
Ivan Shmidt
2020-09-12 19:16:19 +02:00
committed by GitHub
11 changed files with 288 additions and 14 deletions
+1
View File
@@ -34,6 +34,7 @@
"moment": "^2.27.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-lazylog": "^4.5.3",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^15.3.3"
@@ -19,6 +19,7 @@ import {
ActionsListWorkflowRunsForRepoResponseData,
ActionsGetWorkflowResponseData,
ActionsGetWorkflowRunResponseData,
EndpointInterface,
} from '@octokit/types';
export const githubActionsApiRef = createApiRef<GithubActionsApi>({
@@ -75,4 +76,15 @@ export type GithubActionsApi = {
repo: string;
runId: number;
}) => Promise<any>;
downloadJobLogsForWorkflowRun: ({
token,
owner,
repo,
runId,
}: {
token: string;
owner: string;
repo: string;
runId: number;
}) => Promise<EndpointInterface>;
};
@@ -20,6 +20,7 @@ import {
ActionsListWorkflowRunsForRepoResponseData,
ActionsGetWorkflowResponseData,
ActionsGetWorkflowRunResponseData,
EndpointInterface,
} from '@octokit/types';
export class GithubActionsClient implements GithubActionsApi {
@@ -102,4 +103,24 @@ export class GithubActionsClient implements GithubActionsApi {
});
return run.data;
}
async downloadJobLogsForWorkflowRun({
token,
owner,
repo,
runId,
}: {
token: string;
owner: string;
repo: string;
runId: number;
}): Promise<EndpointInterface> {
const workflow = await new Octokit({
auth: token,
}).actions.downloadJobLogsForWorkflowRun({
owner,
repo,
job_id: runId,
});
return workflow.data;
}
}
+1
View File
@@ -29,6 +29,7 @@ export type Job = {
conclusion: string;
started_at: string;
completed_at: string;
id: string;
name: string;
steps: Step[];
};
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { Link } from '@backstage/core';
import {
@@ -45,6 +44,7 @@ import { useProjectName } from '../useProjectName';
import { WorkflowRunStatus } from '../WorkflowRunStatus';
import { useWorkflowRunJobs } from './useWorkflowRunJobs';
import { useWorkflowRunsDetails } from './useWorkflowRunsDetails';
import { WorkflowRunLogs } from '../WorkflowRunLogs';
const useStyles = makeStyles<Theme>(theme => ({
root: {
@@ -57,7 +57,7 @@ const useStyles = makeStyles<Theme>(theme => ({
table: {
padding: theme.spacing(1),
},
expansionPanelDetails: {
accordionDetails: {
padding: 0,
},
button: {
@@ -71,7 +71,7 @@ const useStyles = makeStyles<Theme>(theme => ({
},
}));
const JobsList = ({ jobs }: { jobs?: Jobs }) => {
const JobsList = ({ jobs, entity }: { jobs?: Jobs; entity: Entity }) => {
const classes = useStyles();
return (
<Box>
@@ -83,6 +83,7 @@ const JobsList = ({ jobs }: { jobs?: Jobs }) => {
className={
job.status !== 'success' ? classes.failed : classes.success
}
entity={entity}
/>
))}
</Box>
@@ -111,7 +112,15 @@ const StepView = ({ step }: { step: Step }) => {
);
};
const JobListItem = ({ job, className }: { job: Job; className: string }) => {
const JobListItem = ({
job,
className,
entity,
}: {
job: Job;
className: string;
entity: Entity;
}) => {
const classes = useStyles();
return (
<Accordion TransitionProps={{ unmountOnExit: true }} className={className}>
@@ -127,7 +136,7 @@ const JobListItem = ({ job, className }: { job: Job; className: string }) => {
{job.name} ({getElapsedTime(job.started_at, job.completed_at)})
</Typography>
</AccordionSummary>
<AccordionDetails className={classes.expansionPanelDetails}>
<AccordionDetails className={classes.accordionDetails}>
<TableContainer>
<Table>
{job.steps.map((step: Step) => (
@@ -136,6 +145,11 @@ const JobListItem = ({ job, className }: { job: Job; className: string }) => {
</Table>
</TableContainer>
</AccordionDetails>
{job.status === 'queued' || job.status === 'in_progress' ? (
<WorkflowRunLogs runId={job.id} inProgress entity={entity} />
) : (
<WorkflowRunLogs runId={job.id} inProgress={false} entity={entity} />
)}
</Accordion>
);
};
@@ -218,7 +232,7 @@ export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => {
{jobs.loading ? (
<CircularProgress />
) : (
<JobsList jobs={jobs.value} />
<JobsList jobs={jobs.value} entity={entity} />
)}
</TableCell>
</TableRow>
@@ -0,0 +1,169 @@
/*
* 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 {
Accordion,
AccordionSummary,
CircularProgress,
Fade,
LinearProgress,
makeStyles,
Modal,
Theme,
Tooltip,
Typography,
Zoom,
} from '@material-ui/core';
import React, { Suspense } from 'react';
import { useDownloadWorkflowRunLogs } from './useDownloadWorkflowRunLogs';
import LinePart from 'react-lazylog/build/LinePart';
import { useProjectName } from '../useProjectName';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import DescriptionIcon from '@material-ui/icons/Description';
import { Entity } from '@backstage/catalog-model';
const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog'));
const useStyles = makeStyles<Theme>(() => ({
button: {
order: -1,
marginRight: 0,
marginLeft: '-20px',
},
modal: {
display: 'flex',
alignItems: 'center',
width: '85%',
height: '85%',
justifyContent: 'center',
margin: 'auto',
},
normalLog: {
height: '75vh',
width: '100%',
},
modalLog: {
height: '100%',
width: '100%',
},
}));
const DisplayLog = ({
jobLogs,
className,
}: {
jobLogs: any;
className: string;
}) => {
return (
<Suspense fallback={<LinearProgress />}>
<div className={className}>
<LazyLog
text={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 = ({
entity,
runId,
inProgress,
}: {
entity: Entity;
runId: string;
inProgress: boolean;
}) => {
const classes = useStyles();
const projectName = useProjectName(entity);
const [owner, repo] = projectName.value ? projectName.value.split('/') : [];
const jobLogs = useDownloadWorkflowRunLogs(repo, owner, runId);
const [open, setOpen] = React.useState(false);
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<Accordion TransitionProps={{ unmountOnExit: true }} disabled={inProgress}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`panel-${name}-content`}
id={`panel-${name}-header`}
IconButtonProps={{
className: classes.button,
}}
>
<Typography variant="button">
{jobLogs.loading ? <CircularProgress /> : 'Job Log'}
</Typography>
<Tooltip title="Open Log" TransitionComponent={Zoom} arrow>
<DescriptionIcon
onClick={event => {
event.stopPropagation();
handleOpen();
}}
style={{ marginLeft: 'auto' }}
/>
</Tooltip>
<Modal
className={classes.modal}
onClick={event => event.stopPropagation()}
open={open}
onClose={handleClose}
>
<Fade in={open}>
<DisplayLog
jobLogs={jobLogs.value || undefined}
className={classes.modalLog}
/>
</Fade>
</Modal>
</AccordionSummary>
{jobLogs.value && (
<DisplayLog
jobLogs={jobLogs.value || undefined}
className={classes.normalLog}
/>
)}
</Accordion>
);
};
@@ -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 { WorkflowRunLogs } from './WorkflowRunLogs';
@@ -0,0 +1,40 @@
/*
* 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;
};