diff --git a/ui/src/components/CronEntriesTable.tsx b/ui/src/components/CronEntriesTable.tsx new file mode 100644 index 0000000..8953da4 --- /dev/null +++ b/ui/src/components/CronEntriesTable.tsx @@ -0,0 +1,282 @@ +import React, { useState } from "react"; +import clsx from "clsx"; +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 SyntaxHighlighter from "react-syntax-highlighter"; +import syntaxHighlightStyle from "react-syntax-highlighter/dist/esm/styles/hljs/github"; +import { SortDirection, ColumnConfig } from "../types/table"; +import TableSortLabel from "@material-ui/core/TableSortLabel"; + +const useStyles = makeStyles((theme) => ({ + table: { + minWidth: 650, + }, + noBorder: { + border: "none", + }, + fixedCell: { + position: "sticky", + zIndex: 1, + left: 0, + background: theme.palette.common.white, + }, +})); + +enum SortBy { + EntryId, + Spec, + Type, + Payload, + Options, + NextEnqueue, + PrevEnqueue, +} + +const colConfigs: ColumnConfig[] = [ + { + label: "Entry ID", + key: "entry_id", + sortBy: SortBy.EntryId, + align: "left", + }, + { + label: "Spec", + key: "spec", + sortBy: SortBy.Spec, + align: "left", + }, + { + label: "Type", + key: "type", + sortBy: SortBy.Type, + align: "left", + }, + { + label: "Payload", + key: "payload", + sortBy: SortBy.Payload, + align: "left", + }, + { + label: "Options", + key: "options", + sortBy: SortBy.Options, + align: "left", + }, + { + label: "Next Enqueue", + key: "next_enqueue", + sortBy: SortBy.NextEnqueue, + align: "left", + }, + { + label: "Prev Enqueue", + key: "prev_enqueue", + sortBy: SortBy.PrevEnqueue, + align: "left", + }, +]; + +function createData( + id: string, + spec: string, + type: string, + payload: any, + options: string, + nextEnqueue: string, + prevEnqueue: string +) { + return { id, spec, type, payload, options, nextEnqueue, prevEnqueue }; +} + +const rows = [ + createData( + "da0e15bb-3649-45de-9c36-90b9db744b8a", + "*/5 * * * *", + "email:welcome", + { user_id: 42 }, + "[Queue('email')]", + "In 29s", + "4m31s ago" + ), + createData( + "fi0e10bb-3649-45de-9c36-90b9db744b8a", + "* 1 * * *", + "email:daily_digest", + {}, + "[Queue('email')]", + "In 23h", + "1h ago" + ), + createData( + "ca0e17bv-3649-45de-9c36-90b9db744b8a", + "@every 10m", + "search:reindex", + {}, + "[Queue('index')]", + "In 2m", + "8m ago" + ), + createData( + "we4e15bb-3649-45de-9c36-90b9db744b8a", + "*/5 * * * *", + "janitor", + { user_id: 42 }, + "[Queue('low')]", + "In 29s", + "4m31s ago" + ), +]; + +interface Entry { + id: string; + spec: string; + type: string; + payload: any; + options: string; + nextEnqueue: string; + prevEnqueue: string; +} + +// sortEntries takes a array of entries and return a sorted array. +// It returns a new array and leave the original array untouched. +function sortEntries( + entries: Entry[], + cmpFn: (first: Entry, second: Entry) => number +): Entry[] { + let copy = [...entries]; + copy.sort(cmpFn); + return copy; +} + +export default function CronEntriesTable() { + const classes = useStyles(); + const [sortBy, setSortBy] = useState(SortBy.EntryId); + const [sortDir, setSortDir] = useState(SortDirection.Asc); + + 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 = (e1: Entry, q2: Entry): number => { + let isE1Smaller: boolean; + switch (sortBy) { + case SortBy.EntryId: + if (e1.id === q2.id) return 0; + isE1Smaller = e1.id < q2.id; + break; + case SortBy.Spec: + if (e1.spec === q2.spec) return 0; + isE1Smaller = e1.spec < q2.spec; + break; + case SortBy.Type: + if (e1.type === q2.type) return 0; + isE1Smaller = e1.type < q2.type; + break; + case SortBy.Payload: + if (e1.payload === q2.payload) return 0; + isE1Smaller = e1.payload < q2.payload; + break; + case SortBy.Options: + if (e1.options === q2.options) return 0; + isE1Smaller = e1.options < q2.options; + break; + case SortBy.NextEnqueue: + if (e1.nextEnqueue === q2.nextEnqueue) return 0; + isE1Smaller = e1.nextEnqueue < q2.nextEnqueue; + break; + case SortBy.PrevEnqueue: + if (e1.prevEnqueue === q2.prevEnqueue) return 0; + isE1Smaller = e1.prevEnqueue < q2.prevEnqueue; + break; + default: + // eslint-disable-next-line no-throw-literal + throw `Unexpected order by value: ${sortBy}`; + } + if (sortDir === SortDirection.Asc) { + return isE1Smaller ? -1 : 1; + } else { + return isE1Smaller ? 1 : -1; + } + }; + + return ( + + + + + {colConfigs.map((cfg, i) => ( + + + {cfg.label} + + + ))} + + + + {sortEntries(rows, cmpFunc).map((row, idx) => { + const isLastRow = idx === rows.length - 1; + return ( + + + {row.id} + + + {row.spec} + + + {row.type} + + + + {JSON.stringify(row.payload)} + + + + + {row.options} + + + + {row.nextEnqueue} + + + {row.prevEnqueue} + + + ); + })} + +
+
+ ); +} diff --git a/ui/src/components/QueuesOverviewTable.tsx b/ui/src/components/QueuesOverviewTable.tsx index 585d9b9..05cabdb 100644 --- a/ui/src/components/QueuesOverviewTable.tsx +++ b/ui/src/components/QueuesOverviewTable.tsx @@ -18,6 +18,7 @@ import MoreHorizIcon from "@material-ui/icons/MoreHoriz"; import DeleteQueueConfirmationDialog from "./DeleteQueueConfirmationDialog"; import { Queue } from "../api"; import { queueDetailsPath } from "../paths"; +import { SortDirection, ColumnConfig } from "../types/table"; const useStyles = makeStyles((theme) => ({ table: { @@ -70,19 +71,7 @@ enum SortBy { None, // no sort support } -enum SortDirection { - Asc = "asc", - Desc = "desc", -} - -interface ColumnConfig { - label: string; - key: string; - sortBy: SortBy; - align: "left" | "right" | "center"; -} - -const colConfigs: ColumnConfig[] = [ +const colConfigs: ColumnConfig[] = [ { label: "Queue", key: "queue", sortBy: SortBy.Queue, align: "left" }, { label: "Size", key: "size", sortBy: SortBy.Size, align: "right" }, { label: "Active", key: "active", sortBy: SortBy.Active, align: "right" }, diff --git a/ui/src/types/table.ts b/ui/src/types/table.ts new file mode 100644 index 0000000..c928586 --- /dev/null +++ b/ui/src/types/table.ts @@ -0,0 +1,15 @@ +// SortDirection describes the direction of sort. +export enum SortDirection { + Asc = "asc", + Desc = "desc", +} + +// ColumnConfig is a config for a table column. +// +// T is the enum of sort keys. +export interface ColumnConfig { + label: string; + key: string; + sortBy: T; + align: "left" | "right" | "center"; +} diff --git a/ui/src/views/CronView.tsx b/ui/src/views/CronView.tsx index 84877f1..768a74c 100644 --- a/ui/src/views/CronView.tsx +++ b/ui/src/views/CronView.tsx @@ -1,10 +1,42 @@ import React from "react"; +import Container from "@material-ui/core/Container"; +import { makeStyles } from "@material-ui/core/styles"; +import Grid from "@material-ui/core/Grid"; +import Paper from "@material-ui/core/Paper"; +import CronEntriesTable from "../components/CronEntriesTable"; +import Typography from "@material-ui/core/Typography"; + +const useStyles = makeStyles((theme) => ({ + container: { + paddingTop: theme.spacing(4), + paddingBottom: theme.spacing(4), + }, + paper: { + padding: theme.spacing(2), + display: "flex", + overflow: "auto", + flexDirection: "column", + }, + heading: { + paddingLeft: theme.spacing(2), + }, +})); function CronView() { + const classes = useStyles(); return ( -
-

Cron

-
+ + + + + + Cron Entries + + + + + + ); }