Merge pull request #2172 from NetPenguins/master
GithubActions Logging Implementation
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user