fennec-fe/src/commits/commit-list.tsx
2021-06-05 20:11:00 +08:00

243 lines
6.1 KiB
TypeScript

import { useMutation, useQuery, useSubscription } from "@apollo/client";
import { Link, useResponse, useRouter } from "@curi/react-dom";
import { faPlayCircle, faVial } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
CircularProgress,
Collapse,
IconButton,
LinearProgress,
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 { useSnackbar } from "notistack";
import { complement, equals, find, propEq, takeWhile } from "ramda";
import {
FC,
Fragment,
ReactNode,
useCallback,
useMemo,
useState,
} from "react";
import {
Commit,
CreatePipelineTaskInput,
Pipeline,
PipelineTask,
TaskStatuses,
PipelineUnits,
} from "../generated/graphql";
import { CREATE_PIPELINE_TASK } from "./mutations";
import { COMMITS } from "./queries";
import { SYNC_COMMITS } from "./subscriptions";
interface Props {
pipeline: Pipeline;
}
const useStyles = makeStyles((theme) => ({
root: {
flex: "1 1 100%",
},
nested: {
paddingLeft: theme.spacing(4),
},
}));
export const CommitList: FC<Props> = ({ pipeline }) => {
const { enqueueSnackbar } = useSnackbar();
const { data, loading, refetch } = useQuery<{ commits?: Commit[] }>(COMMITS, {
variables: {
pipelineId: pipeline.id,
},
});
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,
});
}
},
}
);
const classes = useStyles();
return (
<section className={classes.root}>
{(() => {
if (loading) {
return <LinearProgress color="secondary" />;
}
return (
<section>
{syncing && <LinearProgress color="secondary" />}
<List>
{data?.commits?.map((commit) => (
<Item key={commit.hash} commit={commit} pipeline={pipeline} />
))}
</List>
</section>
);
})()}
</section>
);
};
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 [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 handleCreateTask = useCallback(
(unit: PipelineUnits) => {
const _units = [...takeWhile(complement(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 },
}),
});
});
},
[commit, createTask, navigate, pipeline, response, units, url]
);
const actions = useMemo(
() =>
units.map((unit) => {
const pair = find(propEq(0, unit), unitActionPairs);
return (
pair && (
<IconButton
key={unit}
aria-label={pair[2]}
disabled={loading}
onClick={() => handleCreateTask(unit)}
>
{pair[1]}
</IconButton>
)
);
}),
[units, handleCreateTask, loading]
);
return (
<Fragment>
<ListItem button onClick={() => setOpen(!isOpen)}>
<ListItemText
primary={commit.message}
secondary={commit.date && format(commit.date, "yyyy-MM-dd HH:mm:ss")}
/>
<ListItemSecondaryAction>{actions}</ListItemSecondaryAction>
</ListItem>
{loading && <LinearProgress color="secondary" />}
<Collapse in={isOpen} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{commit.tasks.map((task) => (
<Link
key={task.id}
name="pipeline-task-detail"
params={{ ...response.params, taskId: task.id }}
>
<TaskItem task={task} />
</Link>
))}
</List>
</Collapse>
</Fragment>
);
};
const TaskItem: FC<{ task: PipelineTask }> = ({ task }) => {
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:
return (
<CircularProgress style={{ color: theme.palette.secondary.main }} />
);
}
})();
return (
<ListItem button className={classes.nested}>
<ListItemIcon>{statusIcon}</ListItemIcon>
<ListItemText
primary={
task.startedAt && format(task.startedAt, "yyyy-MM-dd HH:mm:ss")
}
/>
</ListItem>
);
};