feat: pipeline-task details

This commit is contained in:
Ivan Li 2021-05-12 21:18:17 +08:00
parent c88e9e6785
commit 73b1c6a40d
7 changed files with 232 additions and 46 deletions

View File

@ -1,13 +1,41 @@
import { useQuery } from '@apollo/client'; import { useMutation, useQuery } from "@apollo/client";
import { Link, useResponse } from '@curi/react-dom'; import { Link, useResponse, useRouter } from '@curi/react-dom';
import { faPlayCircle, faVial } from '@fortawesome/free-solid-svg-icons'; import { faPlayCircle, faVial } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { CircularProgress, Collapse, IconButton, LinearProgress, List, ListItem, ListItemIcon, ListItemSecondaryAction, ListItemText, makeStyles, useTheme } from '@material-ui/core'; import {
import { Cancel, CheckCircle, CloudDownload, ShoppingCart, Timer } from '@material-ui/icons'; CircularProgress,
import { format } from 'date-fns'; Collapse,
import React, { FC, Fragment, ReactNode, useState } from 'react'; IconButton,
import { Commit, Pipeline, PipelineTask, TaskStatuses } from '../generated/graphql'; LinearProgress,
import { COMMITS } from './queries'; List,
ListItem,
ListItemIcon,
ListItemSecondaryAction,
ListItemText,
makeStyles,
useTheme,
} from "@material-ui/core";
import {
Cancel,
CheckCircle,
CloudDownload,
ShoppingCart,
Timer,
} from "@material-ui/icons";
import { format } from "date-fns";
import { equals, find, propEq, takeWhile } from "ramda";
import React, { FC, Fragment, ReactNode, useMemo, useState } from "react";
import {
Commit,
CreatePipelineTaskInput,
Pipeline,
PipelineTask,
TaskStatuses,
WorkUnit,
PipelineUnits,
} from "../generated/graphql";
import { CREATE_PIPELINE_TASK } from "./muations";
import { COMMITS } from "./queries";
interface Props { interface Props {
pipeline: Pipeline; pipeline: Pipeline;
@ -41,20 +69,84 @@ export const CommitList: FC<Props> = ({pipeline}) => {
return ( return (
<List> <List>
{data?.commits.map((commit) => ( {data?.commits.map((commit) => (
<Item key={commit.hash} commit={commit} /> <Item key={commit.hash} commit={commit} pipeline={pipeline} />
))} ))}
</List> </List>
); );
})()} })()}
</section> </section>
) );
} };
const Item: FC<{commit: Commit}> = ({commit}) => { const unitActionPairs: Array<[PipelineUnits, ReactNode, string]> = [
[PipelineUnits.Checkout, <ShoppingCart />, "checkout"],
[
PipelineUnits.InstallDependencies,
<CloudDownload />,
"install dependencies",
],
[PipelineUnits.Test, <FontAwesomeIcon icon={faVial} />, "test"],
[PipelineUnits.Deploy, <FontAwesomeIcon icon={faPlayCircle} />, "deploy"],
];
const Item: FC<{ commit: Commit; pipeline: Pipeline }> = ({
commit,
pipeline,
}) => {
const [isOpen, setOpen] = useState(() => false); const [isOpen, setOpen] = useState(() => false);
const classes = useStyles(); const classes = useStyles();
const [createTask, { loading }] = useMutation<
{ createPipelineTask: PipelineTask },
{ task: CreatePipelineTaskInput }
>(CREATE_PIPELINE_TASK);
const units = useMemo(
() => pipeline.workUnitMetadata.units.map((unit) => unit.type),
[pipeline]
);
const {navigate, url} = useRouter();
const { response } = useResponse(); const { response } = useResponse();
const handleCreateTask = (unit: PipelineUnits) => {
const _units = [...takeWhile(equals(unit), units), unit];
createTask({
variables: {
task: {
units: _units,
pipelineId: pipeline.id,
commit: commit.hash,
},
},
}).then(({data}) => {
navigate({
url: url({
name: "pipeline-task-detail",
params: { ...response.params, taskId: data?.createPipelineTask.id},
}),
});
});
};
const actions = useMemo(
() =>
units.map((unit) => {
const pair = find(propEq(0, unit), unitActionPairs);
return (
pair && (
<IconButton
aria-label={pair[2]}
disabled={loading}
onClick={() => handleCreateTask(unit)}
>
{pair[1]}
</IconButton>
)
);
}),
[units]
);
return ( return (
<Fragment> <Fragment>
<ListItem button onClick={() => setOpen(!isOpen)}> <ListItem button onClick={() => setOpen(!isOpen)}>
@ -62,21 +154,9 @@ const Item: FC<{commit: Commit}> = ({commit}) => {
primary={commit.message} primary={commit.message}
secondary={format(commit.date, "yyyy-MM-dd HH:mm:ss")} secondary={format(commit.date, "yyyy-MM-dd HH:mm:ss")}
/> />
<ListItemSecondaryAction> <ListItemSecondaryAction>{actions}</ListItemSecondaryAction>
<IconButton edge="end" aria-label="delete">
<ShoppingCart />
</IconButton>
<IconButton edge="end" aria-label="delete">
<CloudDownload />
</IconButton>
<IconButton edge="end" aria-label="delete">
<FontAwesomeIcon icon={faVial} />
</IconButton>
<IconButton edge="end" aria-label="delete">
<FontAwesomeIcon icon={faPlayCircle} />
</IconButton>
</ListItemSecondaryAction>
</ListItem> </ListItem>
{loading && <LinearProgress color="secondary" />}
<Collapse in={isOpen} timeout="auto" unmountOnExit> <Collapse in={isOpen} timeout="auto" unmountOnExit>
<List component="div" disablePadding> <List component="div" disablePadding>
{commit.tasks.map((task) => ( {commit.tasks.map((task) => (
@ -92,7 +172,7 @@ const Item: FC<{commit: Commit}> = ({commit}) => {
</Collapse> </Collapse>
</Fragment> </Fragment>
); );
} };
const TaskItem: FC<{ task: PipelineTask }> = ({ task }) => { const TaskItem: FC<{ task: PipelineTask }> = ({ task }) => {
const classes = useStyles(); const classes = useStyles();
@ -106,11 +186,11 @@ const TaskItem: FC<{task: PipelineTask}> = ({task}) => {
case TaskStatuses.Failed: case TaskStatuses.Failed:
return <Cancel style={{ color: theme.palette.error.main }} />; return <Cancel style={{ color: theme.palette.error.main }} />;
case TaskStatuses.Working: case TaskStatuses.Working:
return <CircularProgress style={{ color: theme.palette.secondary.main }} />; return (
<CircularProgress style={{ color: theme.palette.secondary.main }} />
);
} }
} })();
)()
return ( return (
<ListItem button className={classes.nested}> <ListItem button className={classes.nested}>
<ListItemIcon>{statusIcon}</ListItemIcon> <ListItemIcon>{statusIcon}</ListItemIcon>

12
src/commits/muations.ts Normal file
View File

@ -0,0 +1,12 @@
import { gql } from '@apollo/client';
export const CREATE_PIPELINE_TASK = gql`
mutation CreatePipelineTask($task: CreatePipelineTaskInput!) {
createPipelineTask(task: $task) {
id
status
startedAt
endedAt
}
}
`

View File

@ -0,0 +1,11 @@
import { Typography } from '@material-ui/core';
import React, { FC } from 'react';
export const ErrorPage: FC = ({children}) => {
return (
<section>
<Typography component="h2">Something is wrong :(</Typography>
<Typography variant="body1" component="div">{children}</Typography>
</section>
);
}

View File

@ -1710,7 +1710,7 @@
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "findPipelineTask", "name": "pipelineTask",
"description": null, "description": null,
"args": [ "args": [
{ {

View File

@ -186,7 +186,7 @@ export type Query = {
pipeline: Pipeline; pipeline: Pipeline;
commits: Array<Commit>; commits: Array<Commit>;
listPipelineTaskByPipelineId: Array<PipelineTask>; listPipelineTaskByPipelineId: Array<PipelineTask>;
findPipelineTask: PipelineTask; pipelineTask: PipelineTask;
}; };
@ -215,7 +215,7 @@ export type QueryListPipelineTaskByPipelineIdArgs = {
}; };
export type QueryFindPipelineTaskArgs = { export type QueryPipelineTaskArgs = {
id: Scalars['String']; id: Scalars['String'];
}; };

View File

@ -1,9 +1,92 @@
import { FC } from 'react' import { gql, useQuery } from "@apollo/client";
import { LinearProgress, makeStyles, Typography } from "@material-ui/core";
import { format } from 'date-fns';
import React, { FC } from "react";
import { ErrorPage } from "../commons/fallbacks/error-page";
import { PipelineTask, PipelineTaskLogs } from "../generated/graphql";
interface Props { interface Props {
taskId: string; taskId: string;
} }
export const PipelineTaskDetail: FC<Props> = ({taskId}) => { const PIPELINE_TASK = gql`
return <div>{taskId} detail</div> query FindPipelineTask($taskId: String!) {
pipelineTask(id: $taskId) {
id
units
commit
status
startedAt
endedAt
logs {
unit
logs
status
startedAt
endedAt
} }
}
}
`;
const useStyles = makeStyles((theme) => ({
root: {
},
groupTitle: {
backgroundColor: 'white',
fontWeight: 500,
borderTop: '1px solid #eee',
fontSize: '16px',
padding: '12px',
marginLeft: '1px',
},
logText: {
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
whiteSpace: 'pre-wrap',
border: 'none',
margin: '6px 12px',
display: "block"
},
}));
export const PipelineTaskDetail: FC<Props> = ({ taskId }) => {
const { data, loading, error } = useQuery<{ pipelineTask: PipelineTask }>(
PIPELINE_TASK,
{
variables: { taskId },
}
);
const classes = useStyles();
if (error) {
return <ErrorPage>{error.message}</ErrorPage>;
}
if (loading) {
return <LinearProgress color="secondary" />;
}
return (
<div className={classes.root}>
<Typography variant="h4" component="h2">
{taskId} detail
</Typography>
{data?.pipelineTask.logs.map((logs) => (
<LogGroup key={logs.unit} logs={logs} />
))}
</div>
);
};
const LogGroup: FC<{ logs: PipelineTaskLogs }> = ({ logs }) => {
const classes = useStyles();
return (
<div>
<div className={classes.groupTitle}>
{logs.unit} {format(logs.startedAt, 'yyyy-MM-dd HH:mm:ss')} {logs.status}
</div>
<code className={classes.logText}>{logs.logs}</code>
</div>
);
};

View File

@ -60,12 +60,12 @@ export const ProjectDetail: FC<Props> = ({ project, children }) => {
alignItems="stretch" alignItems="stretch"
className={classes.root} className={classes.root}
> >
<Grid item xs={3} style={{ height: "100%", display: "flex" }}> <Grid item lg={1} style={{ height: "100%", display: "flex" }}>
<Paper className={classes.pipelineListContainer}> <Paper className={classes.pipelineListContainer}>
<PipelineList projectId={project.id} /> <PipelineList projectId={project.id} />
</Paper> </Paper>
</Grid> </Grid>
<Grid item xs={9} style={{ height: "100%", display: "flex", overflowY: 'auto' }}> <Grid item xs={10} lg={11} style={{ height: "100%", display: "flex", overflowY: 'auto' }}>
{children} {children}
</Grid> </Grid>
</Grid> </Grid>