fennec-fe/src/commits/commit-list.tsx

275 lines
6.9 KiB
TypeScript
Raw Normal View History

2021-06-05 19:17:45 +08:00
import { useMutation, useQuery, useSubscription } from "@apollo/client";
import { Link, useResponse, useRouter } from "@curi/react-dom";
2021-05-12 21:18:17 +08:00
import { faPlayCircle, faVial } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
2021-06-20 15:42:03 +08:00
Backdrop,
2021-05-12 21:18:17 +08:00
CircularProgress,
Collapse,
IconButton,
LinearProgress,
List,
ListItem,
ListItemIcon,
ListItemSecondaryAction,
ListItemText,
makeStyles,
useTheme,
2021-06-20 15:42:03 +08:00
withStyles,
2021-05-12 21:18:17 +08:00
} from "@material-ui/core";
import {
Cancel,
CheckCircle,
CloudDownload,
ShoppingCart,
2021-06-20 15:42:03 +08:00
Stop,
2021-05-12 21:18:17 +08:00
Timer,
} from "@material-ui/icons";
import { format } from "date-fns";
2021-06-05 19:17:45 +08:00
import { useSnackbar } from "notistack";
import { complement, equals, find, propEq, takeWhile } from "ramda";
import {
FC,
Fragment,
2021-06-20 15:42:03 +08:00
MouseEventHandler,
2021-06-05 19:17:45 +08:00
ReactNode,
useCallback,
useMemo,
useState,
} from "react";
2021-05-12 21:18:17 +08:00
import {
Commit,
CreatePipelineTaskInput,
Pipeline,
PipelineTask,
TaskStatuses,
PipelineUnits,
} from "../generated/graphql";
2021-06-20 15:42:03 +08:00
import { CREATE_PIPELINE_TASK, STOP_PIPELINE_TASK } from "./mutations";
2021-05-12 21:18:17 +08:00
import { COMMITS } from "./queries";
2021-06-05 19:17:45 +08:00
import { SYNC_COMMITS } from "./subscriptions";
2021-05-09 15:29:11 +08:00
interface Props {
pipeline: Pipeline;
}
const useStyles = makeStyles((theme) => ({
root: {
flex: "1 1 100%",
},
nested: {
paddingLeft: theme.spacing(4),
},
}));
2021-05-12 21:18:17 +08:00
export const CommitList: FC<Props> = ({ pipeline }) => {
2021-06-05 19:17:45 +08:00
const { enqueueSnackbar } = useSnackbar();
const { data, loading, refetch } = useQuery<{ commits?: Commit[] }>(COMMITS, {
2021-05-09 15:29:11 +08:00
variables: {
pipelineId: pipeline.id,
},
});
2021-06-05 19:17:45 +08:00
const { loading: syncing } = useSubscription<{ syncCommits: boolean }>(
SYNC_COMMITS,
{
variables: {
pipelineId: pipeline.id,
},
onSubscriptionData({ subscriptionData: { data, error } }) {
if (error) {
enqueueSnackbar(error.message, {
variant: "warning",
});
}
if (data?.syncCommits) {
refetch({
appInstance: data.syncCommits,
});
}
},
}
);
2021-05-09 15:29:11 +08:00
const classes = useStyles();
return (
<section className={classes.root}>
{(() => {
if (loading) {
return <LinearProgress color="secondary" />;
}
2021-05-09 16:42:19 +08:00
return (
2021-06-05 19:17:45 +08:00
<section>
{syncing && <LinearProgress color="secondary" />}
<List>
{data?.commits?.map((commit) => (
<Item key={commit.hash} commit={commit} pipeline={pipeline} />
))}
</List>
</section>
2021-05-09 16:42:19 +08:00
);
2021-05-09 15:29:11 +08:00
})()}
</section>
2021-05-12 21:18:17 +08:00
);
};
2021-05-09 15:29:11 +08:00
2021-05-12 21:18:17 +08:00
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,
}) => {
2021-05-09 15:29:11 +08:00
const [isOpen, setOpen] = useState(() => false);
2021-05-09 16:42:19 +08:00
2021-06-05 19:17:45 +08:00
const [createTask, { loading }] =
useMutation<
{ createPipelineTask: PipelineTask },
{ task: CreatePipelineTaskInput }
>(CREATE_PIPELINE_TASK);
2021-05-12 21:18:17 +08:00
const units = useMemo(
() => pipeline.workUnitMetadata.units.map((unit) => unit.type),
[pipeline]
);
2021-06-05 19:17:45 +08:00
const { navigate, url } = useRouter();
2021-05-12 21:18:17 +08:00
const { response } = useResponse();
2021-06-05 19:17:45 +08:00
const handleCreateTask = useCallback(
(unit: PipelineUnits) => {
const _units = [...takeWhile(complement(equals(unit)), units), unit];
createTask({
variables: {
task: {
units: _units,
pipelineId: pipeline.id,
commit: commit.hash,
},
2021-05-12 21:18:17 +08:00
},
2021-06-05 19:17:45 +08:00
}).then(({ data }) => {
navigate({
url: url({
name: "pipeline-task-detail",
params: { ...response.params, taskId: data?.createPipelineTask.id },
}),
});
2021-05-12 21:18:17 +08:00
});
2021-06-05 19:17:45 +08:00
},
[commit, createTask, navigate, pipeline, response, units, url]
);
2021-05-12 21:18:17 +08:00
const actions = useMemo(
() =>
units.map((unit) => {
const pair = find(propEq(0, unit), unitActionPairs);
return (
pair && (
<IconButton
2021-06-05 19:17:45 +08:00
key={unit}
2021-05-12 21:18:17 +08:00
aria-label={pair[2]}
disabled={loading}
onClick={() => handleCreateTask(unit)}
>
{pair[1]}
</IconButton>
)
);
}),
2021-06-05 19:17:45 +08:00
[units, handleCreateTask, loading]
2021-05-12 21:18:17 +08:00
);
2021-05-09 15:29:11 +08:00
return (
<Fragment>
<ListItem button onClick={() => setOpen(!isOpen)}>
<ListItemText
primary={commit.message}
2021-06-05 19:17:45 +08:00
secondary={commit.date && format(commit.date, "yyyy-MM-dd HH:mm:ss")}
2021-05-09 15:29:11 +08:00
/>
2021-05-12 21:18:17 +08:00
<ListItemSecondaryAction>{actions}</ListItemSecondaryAction>
2021-05-09 15:29:11 +08:00
</ListItem>
2021-05-12 21:18:17 +08:00
{loading && <LinearProgress color="secondary" />}
2021-05-09 15:29:11 +08:00
<Collapse in={isOpen} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
2021-05-09 16:42:19 +08:00
{commit.tasks.map((task) => (
<Link
key={task.id}
name="pipeline-task-detail"
params={{ ...response.params, taskId: task.id }}
>
<TaskItem task={task} />
</Link>
))}
2021-05-09 15:29:11 +08:00
</List>
</Collapse>
</Fragment>
);
2021-05-12 21:18:17 +08:00
};
2021-05-09 15:29:11 +08:00
2021-05-12 21:18:17 +08:00
const TaskItem: FC<{ task: PipelineTask }> = ({ task }) => {
2021-05-09 15:29:11 +08:00
const classes = useStyles();
const theme = useTheme();
const statusIcon: ReactNode = (() => {
switch (task.status) {
case TaskStatuses.Pending:
return <Timer style={{ color: theme.palette.info.main }} />;
case TaskStatuses.Success:
return <CheckCircle style={{ color: theme.palette.success.main }} />;
case TaskStatuses.Failed:
return <Cancel style={{ color: theme.palette.error.main }} />;
case TaskStatuses.Working:
2021-05-12 21:18:17 +08:00
return (
<CircularProgress style={{ color: theme.palette.secondary.main }} />
);
2021-05-09 15:29:11 +08:00
}
2021-05-12 21:18:17 +08:00
})();
2021-06-20 15:42:03 +08:00
const [stopTask, { loading: stopTaskWaiting }] = useMutation(
STOP_PIPELINE_TASK,
{
variables: { taskId: task.id },
}
);
const stop: MouseEventHandler = useCallback(
(event) => {
event.stopPropagation();
event.preventDefault();
stopTask();
},
[stopTask]
);
2021-05-09 15:29:11 +08:00
return (
<ListItem button className={classes.nested}>
<ListItemIcon>{statusIcon}</ListItemIcon>
2021-06-05 19:17:45 +08:00
<ListItemText
primary={
task.startedAt && format(task.startedAt, "yyyy-MM-dd HH:mm:ss")
}
/>
2021-06-20 15:42:03 +08:00
<ListItemSecondaryAction>
{task.status === TaskStatuses.Working && (
<IconButton edge="end" aria-label="stop" onClick={stop}>
<Stop />
</IconButton>
)}
</ListItemSecondaryAction>
<LimitedBackdrop open={stopTaskWaiting} />
2021-05-09 15:29:11 +08:00
</ListItem>
);
2021-05-12 21:18:17 +08:00
};
2021-06-20 15:42:03 +08:00
const LimitedBackdrop = withStyles({
root: {
position: "absolute",
zIndex: 1,
},
})(Backdrop);