Initial commit

This commit is contained in:
Ken Hibino
2020-11-24 06:54:00 -08:00
commit 7bd35a88e5
51 changed files with 16522 additions and 0 deletions

View File

@@ -0,0 +1,348 @@
import React, { useState } from "react";
import clsx from "clsx";
import { Link } from "react-router-dom";
import { makeStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableFooter from "@material-ui/core/TableFooter";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import IconButton from "@material-ui/core/IconButton";
import PauseCircleFilledIcon from "@material-ui/icons/PauseCircleFilled";
import PlayCircleFilledIcon from "@material-ui/icons/PlayCircleFilled";
import { Queue } from "../api";
import { queueDetailsPath } from "../paths";
const useStyles = makeStyles((theme) => ({
table: {
minWidth: 650,
},
linkCell: {
textDecoration: "none",
},
footerCell: {
fontWeight: 600,
fontSize: "0.875rem",
borderBottom: "none",
},
boldCell: {
fontWeight: 600,
},
fixedCell: {
position: "sticky",
zIndex: 1,
left: 0,
background: theme.palette.common.white,
},
}));
interface QueueWithMetadata extends Queue {
pauseRequestPending: boolean; // indicates pause/resume request is pending for the queue.
}
interface Props {
queues: QueueWithMetadata[];
onPauseClick: (qname: string) => Promise<void>;
onResumeClick: (qname: string) => Promise<void>;
}
enum SortBy {
Queue,
Size,
Active,
Pending,
Scheduled,
Retry,
Dead,
Processed,
Succeeded,
Failed,
}
enum SortDirection {
Asc = "asc",
Desc = "desc",
}
const columnConfig = [
{ label: "Queue", key: "queue", sortBy: SortBy.Queue },
{ label: "Size", key: "size", sortBy: SortBy.Size },
{ label: "Active", key: "active", sortBy: SortBy.Active },
{ label: "Pending", key: "pending", sortBy: SortBy.Pending },
{ label: "Scheduled", key: "scheduled", sortBy: SortBy.Scheduled },
{ label: "Retry", key: "retry", sortBy: SortBy.Retry },
{ label: "Dead", key: "dead", sortBy: SortBy.Dead },
{ label: "Processed", key: "processed", sortBy: SortBy.Processed },
{ label: "Succeeded", key: "Succeeded", sortBy: SortBy.Succeeded },
{ label: "Failed", key: "failed", sortBy: SortBy.Failed },
];
// sortQueues takes a array of queues and return a sorted array.
// It returns a new array and leave the original array untouched.
function sortQueues(
queues: QueueWithMetadata[],
cmpFn: (first: QueueWithMetadata, second: QueueWithMetadata) => number
): QueueWithMetadata[] {
let copy = [...queues];
copy.sort(cmpFn);
return copy;
}
export default function QueuesOverviewTable(props: Props) {
const classes = useStyles();
const [sortBy, setSortBy] = useState<SortBy>(SortBy.Queue);
const [sortDir, setSortDir] = useState<SortDirection>(SortDirection.Asc);
const total = getAggregateCounts(props.queues);
const createSortClickHandler = (sortKey: SortBy) => (e: React.MouseEvent) => {
if (sortKey === sortBy) {
// Toggle sort direction.
const nextSortDir =
sortDir === SortDirection.Asc ? SortDirection.Desc : SortDirection.Asc;
setSortDir(nextSortDir);
} else {
// Change the sort key.
setSortBy(sortKey);
}
};
const cmpFunc = (q1: QueueWithMetadata, q2: QueueWithMetadata): number => {
let isQ1Smaller: boolean;
switch (sortBy) {
case SortBy.Queue:
if (q1.queue === q2.queue) return 0;
isQ1Smaller = q1.queue < q2.queue;
break;
case SortBy.Size:
if (q1.size === q2.size) return 0;
isQ1Smaller = q1.size < q2.size;
break;
case SortBy.Active:
if (q1.active === q2.active) return 0;
isQ1Smaller = q1.active < q2.active;
break;
case SortBy.Pending:
if (q1.pending === q2.pending) return 0;
isQ1Smaller = q1.pending < q2.pending;
break;
case SortBy.Scheduled:
if (q1.scheduled === q2.scheduled) return 0;
isQ1Smaller = q1.scheduled < q2.scheduled;
break;
case SortBy.Retry:
if (q1.retry === q2.retry) return 0;
isQ1Smaller = q1.retry < q2.retry;
break;
case SortBy.Dead:
if (q1.dead === q2.dead) return 0;
isQ1Smaller = q1.dead < q2.dead;
break;
case SortBy.Processed:
if (q1.processed === q2.processed) return 0;
isQ1Smaller = q1.processed < q2.processed;
break;
case SortBy.Succeeded:
const q1Succeeded = q1.processed - q1.failed;
const q2Succeeded = q2.processed - q2.failed;
if (q1Succeeded === q2Succeeded) return 0;
isQ1Smaller = q1Succeeded < q2Succeeded;
break;
case SortBy.Failed:
if (q1.failed === q2.failed) return 0;
isQ1Smaller = q1.failed < q2.failed;
break;
default:
// eslint-disable-next-line no-throw-literal
throw `Unexpected order by value: ${sortBy}`;
}
if (sortDir === SortDirection.Asc) {
return isQ1Smaller ? -1 : 1;
} else {
return isQ1Smaller ? 1 : -1;
}
};
return (
<TableContainer>
<Table className={classes.table} aria-label="queues overview table">
<TableHead>
<TableRow>
{columnConfig.map((cfg, i) => (
<TableCell
key={cfg.key}
align={i === 0 ? "left" : "right"}
className={clsx(i === 0 && classes.fixedCell)}
>
<TableSortLabel
active={sortBy === cfg.sortBy}
direction={sortDir}
onClick={createSortClickHandler(cfg.sortBy)}
>
{cfg.label}
</TableSortLabel>
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{sortQueues(props.queues, cmpFunc).map((q) => (
<TableRow key={q.queue}>
<TableCell
component="th"
scope="row"
className={clsx(classes.boldCell, classes.fixedCell)}
>
<Link to={queueDetailsPath(q.queue)}>
{q.queue}
{q.paused ? " (paused)" : ""}
</Link>
</TableCell>
<TableCell align="right" className={classes.boldCell}>
{q.size}
</TableCell>
<TableCell align="right">
<Link
to={queueDetailsPath(q.queue, "active")}
className={classes.linkCell}
>
{q.active}
</Link>
</TableCell>
<TableCell align="right">
<Link
to={queueDetailsPath(q.queue, "pending")}
className={classes.linkCell}
>
{q.pending}
</Link>
</TableCell>
<TableCell align="right">
<Link
to={queueDetailsPath(q.queue, "scheduled")}
className={classes.linkCell}
>
{q.scheduled}
</Link>
</TableCell>
<TableCell align="right">
<Link
to={queueDetailsPath(q.queue, "retry")}
className={classes.linkCell}
>
{q.retry}
</Link>
</TableCell>
<TableCell align="right">
<Link
to={queueDetailsPath(q.queue, "dead")}
className={classes.linkCell}
>
{q.dead}
</Link>
</TableCell>
<TableCell align="right" className={classes.boldCell}>
{q.processed}
</TableCell>
<TableCell align="right">{q.processed - q.failed}</TableCell>
<TableCell align="right">{q.failed}</TableCell>
{/* <TableCell align="right">
{q.paused ? (
<IconButton
color="secondary"
onClick={() => props.onResumeClick(q.queue)}
disabled={q.pauseRequestPending}
>
<PlayCircleFilledIcon />
</IconButton>
) : (
<IconButton
color="primary"
onClick={() => props.onPauseClick(q.queue)}
disabled={q.pauseRequestPending}
>
<PauseCircleFilledIcon />
</IconButton>
)}
</TableCell> */}
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell className={clsx(classes.fixedCell, classes.footerCell)}>
Total
</TableCell>
<TableCell className={classes.footerCell} align="right">
{total.size}
</TableCell>
<TableCell className={classes.footerCell} align="right">
{total.active}
</TableCell>
<TableCell className={classes.footerCell} align="right">
{total.pending}
</TableCell>
<TableCell className={classes.footerCell} align="right">
{total.scheduled}
</TableCell>
<TableCell className={classes.footerCell} align="right">
{total.retry}
</TableCell>
<TableCell className={classes.footerCell} align="right">
{total.dead}
</TableCell>
<TableCell className={classes.footerCell} align="right">
{total.processed}
</TableCell>
<TableCell className={classes.footerCell} align="right">
{total.succeeded}
</TableCell>
<TableCell className={classes.footerCell} align="right">
{total.failed}
</TableCell>
</TableRow>
</TableFooter>
</Table>
</TableContainer>
);
}
interface AggregateCounts {
size: number;
active: number;
pending: number;
scheduled: number;
retry: number;
dead: number;
processed: number;
succeeded: number;
failed: number;
}
function getAggregateCounts(queues: Queue[]): AggregateCounts {
let total = {
size: 0,
active: 0,
pending: 0,
scheduled: 0,
retry: 0,
dead: 0,
processed: 0,
succeeded: 0,
failed: 0,
};
queues.forEach((q) => {
total.size += q.size;
total.active += q.active;
total.pending += q.pending;
total.scheduled += q.scheduled;
total.retry += q.retry;
total.dead += q.dead;
total.processed += q.processed;
total.succeeded += q.processed - q.failed;
total.failed += q.failed;
});
return total;
}