github.com/wfusion/gofusion@v1.1.14/common/infra/asynq/asynqmon/ui/src/components/SchedulerEntriesTable.tsx (about)

     1  import React, { useState } from "react";
     2  import clsx from "clsx";
     3  import { makeStyles } from "@material-ui/core/styles";
     4  import IconButton from "@material-ui/core/IconButton";
     5  import Table from "@material-ui/core/Table";
     6  import TableBody from "@material-ui/core/TableBody";
     7  import TableCell from "@material-ui/core/TableCell";
     8  import TableContainer from "@material-ui/core/TableContainer";
     9  import TableHead from "@material-ui/core/TableHead";
    10  import TableRow from "@material-ui/core/TableRow";
    11  import Modal from "@material-ui/core/Modal";
    12  import Typography from "@material-ui/core/Typography";
    13  import Tooltip from "@material-ui/core/Tooltip";
    14  import HistoryIcon from "@material-ui/icons/History";
    15  import Alert from "@material-ui/lab/Alert";
    16  import AlertTitle from "@material-ui/lab/AlertTitle";
    17  import { SortDirection, SortableTableColumn } from "../types/table";
    18  import TableSortLabel from "@material-ui/core/TableSortLabel";
    19  import SyntaxHighlighter from "./SyntaxHighlighter";
    20  import { SchedulerEntry } from "../api";
    21  import { timeAgo, durationBefore, prettifyPayload } from "../utils";
    22  import SchedulerEnqueueEventsTable from "./SchedulerEnqueueEventsTable";
    23  
    24  const useStyles = makeStyles((theme) => ({
    25    table: {
    26      minWidth: 650,
    27    },
    28    noBorder: {
    29      border: "none",
    30    },
    31    fixedCell: {
    32      position: "sticky",
    33      zIndex: 1,
    34      left: 0,
    35      background: theme.palette.background.paper,
    36    },
    37    modal: {
    38      display: "flex",
    39      alignItems: "center",
    40      justifyContent: "center",
    41    },
    42    modalContent: {
    43      background: theme.palette.background.paper,
    44      padding: theme.spacing(2),
    45      width: "540px",
    46      outline: "none",
    47      borderRadius: theme.shape.borderRadius,
    48    },
    49    eventsTable: {
    50      maxHeight: "80vh",
    51    },
    52  }));
    53  
    54  enum SortBy {
    55    EntryId,
    56    Spec,
    57    Type,
    58    Payload,
    59    Options,
    60    NextEnqueue,
    61    PrevEnqueue,
    62  
    63    None,
    64  }
    65  
    66  const colConfigs: SortableTableColumn<SortBy>[] = [
    67    {
    68      label: "Entry ID",
    69      key: "entry_id",
    70      sortBy: SortBy.EntryId,
    71      align: "left",
    72    },
    73    {
    74      label: "Spec",
    75      key: "spec",
    76      sortBy: SortBy.Spec,
    77      align: "left",
    78    },
    79    {
    80      label: "Type",
    81      key: "type",
    82      sortBy: SortBy.Type,
    83      align: "left",
    84    },
    85    {
    86      label: "Payload",
    87      key: "task_payload",
    88      sortBy: SortBy.Payload,
    89      align: "left",
    90    },
    91    {
    92      label: "Options",
    93      key: "options",
    94      sortBy: SortBy.Options,
    95      align: "left",
    96    },
    97    {
    98      label: "Next Enqueue",
    99      key: "next_enqueue",
   100      sortBy: SortBy.NextEnqueue,
   101      align: "left",
   102    },
   103    {
   104      label: "Prev Enqueue",
   105      key: "prev_enqueue",
   106      sortBy: SortBy.PrevEnqueue,
   107      align: "left",
   108    },
   109    {
   110      label: "",
   111      key: "show_history",
   112      sortBy: SortBy.None,
   113      align: "left",
   114    },
   115  ];
   116  
   117  // sortEntries takes a array of entries and return a sorted array.
   118  // It returns a new array and leave the original array untouched.
   119  function sortEntries(
   120    entries: SchedulerEntry[],
   121    cmpFn: (first: SchedulerEntry, second: SchedulerEntry) => number
   122  ): SchedulerEntry[] {
   123    let copy = [...entries];
   124    copy.sort(cmpFn);
   125    return copy;
   126  }
   127  
   128  interface Props {
   129    entries: SchedulerEntry[];
   130  }
   131  
   132  export default function SchedulerEntriesTable(props: Props) {
   133    const classes = useStyles();
   134    const [sortBy, setSortBy] = useState<SortBy>(SortBy.EntryId);
   135    const [sortDir, setSortDir] = useState<SortDirection>(SortDirection.Asc);
   136    const [activeEntryId, setActiveEntryId] = useState<string>("");
   137  
   138    const createSortClickHandler = (sortKey: SortBy) => (e: React.MouseEvent) => {
   139      if (sortKey === sortBy) {
   140        // Toggle sort direction.
   141        const nextSortDir =
   142          sortDir === SortDirection.Asc ? SortDirection.Desc : SortDirection.Asc;
   143        setSortDir(nextSortDir);
   144      } else {
   145        // Change the sort key.
   146        setSortBy(sortKey);
   147      }
   148    };
   149  
   150    const cmpFunc = (e1: SchedulerEntry, e2: SchedulerEntry): number => {
   151      let isE1Smaller: boolean;
   152      switch (sortBy) {
   153        case SortBy.EntryId:
   154          if (e1.id === e2.id) return 0;
   155          isE1Smaller = e1.id < e2.id;
   156          break;
   157        case SortBy.Spec:
   158          if (e1.spec === e2.spec) return 0;
   159          isE1Smaller = e1.spec < e2.spec;
   160          break;
   161        case SortBy.Type:
   162          if (e1.task_type === e2.task_type) return 0;
   163          isE1Smaller = e1.task_type < e2.task_type;
   164          break;
   165        case SortBy.Payload:
   166          if (e1.task_payload === e2.task_payload) return 0;
   167          isE1Smaller = e1.task_payload < e2.task_payload;
   168          break;
   169        case SortBy.Options:
   170          if (e1.options === e2.options) return 0;
   171          isE1Smaller = e1.options < e2.options;
   172          break;
   173        case SortBy.NextEnqueue:
   174          if (e1.next_enqueue_at === e2.next_enqueue_at) return 0;
   175          isE1Smaller = e1.next_enqueue_at < e2.next_enqueue_at;
   176          break;
   177        case SortBy.PrevEnqueue:
   178          const e1PrevEnqueueAt = e1.prev_enqueue_at || "";
   179          const e2PrevEnqueueAt = e2.prev_enqueue_at || "";
   180          if (e1PrevEnqueueAt === e2PrevEnqueueAt) return 0;
   181          isE1Smaller = e1PrevEnqueueAt < e2PrevEnqueueAt;
   182          break;
   183        default:
   184          // eslint-disable-next-line no-throw-literal
   185          throw `Unexpected order by value: ${sortBy}`;
   186      }
   187      if (sortDir === SortDirection.Asc) {
   188        return isE1Smaller ? -1 : 1;
   189      } else {
   190        return isE1Smaller ? 1 : -1;
   191      }
   192    };
   193  
   194    if (props.entries.length === 0) {
   195      return (
   196        <Alert severity="info">
   197          <AlertTitle>Info</AlertTitle>
   198          No entries found at this time.
   199        </Alert>
   200      );
   201    }
   202  
   203    return (
   204      <>
   205        <TableContainer>
   206          <Table className={classes.table} aria-label="scheduler entries table">
   207            <TableHead>
   208              <TableRow>
   209                {colConfigs.map((cfg, i) => (
   210                  <TableCell
   211                    key={cfg.key}
   212                    align={cfg.align}
   213                    className={clsx(i === 0 && classes.fixedCell)}
   214                  >
   215                    <TableSortLabel
   216                      active={cfg.sortBy === sortBy}
   217                      direction={sortDir}
   218                      onClick={createSortClickHandler(cfg.sortBy)}
   219                    >
   220                      {cfg.label}
   221                    </TableSortLabel>
   222                  </TableCell>
   223                ))}
   224              </TableRow>
   225            </TableHead>
   226            <TableBody>
   227              {sortEntries(props.entries, cmpFunc).map((entry, idx) => (
   228                <Row
   229                  key={entry.id}
   230                  entry={entry}
   231                  isLastRow={idx === props.entries.length - 1}
   232                  onShowHistoryClick={() => setActiveEntryId(entry.id)}
   233                />
   234              ))}
   235            </TableBody>
   236          </Table>
   237          <Modal
   238            open={activeEntryId !== ""}
   239            onClose={() => setActiveEntryId("")}
   240            className={classes.modal}
   241          >
   242            <div className={classes.modalContent}>
   243              <Typography variant="h6" gutterBottom color="textPrimary">
   244                Recent History
   245              </Typography>
   246              <SchedulerEnqueueEventsTable entryId={activeEntryId} />
   247            </div>
   248          </Modal>
   249        </TableContainer>
   250      </>
   251    );
   252  }
   253  
   254  interface RowProps {
   255    entry: SchedulerEntry;
   256    isLastRow: boolean;
   257    onShowHistoryClick: () => void;
   258  }
   259  
   260  const useRowStyles = makeStyles((theme) => ({
   261    rowRoot: {
   262      "& > *": {
   263        borderBottom: "unset",
   264      },
   265    },
   266    noBorder: {
   267      border: "none",
   268    },
   269  }));
   270  
   271  function Row(props: RowProps) {
   272    const { entry, isLastRow } = props;
   273    const classes = useRowStyles();
   274    return (
   275      <TableRow className={classes.rowRoot}>
   276        <TableCell
   277          component="th"
   278          scope="row"
   279          className={clsx(isLastRow && classes.noBorder)}
   280        >
   281          {entry.id}
   282        </TableCell>
   283        <TableCell className={clsx(isLastRow && classes.noBorder)}>
   284          {entry.spec}
   285        </TableCell>
   286        <TableCell className={clsx(isLastRow && classes.noBorder)}>
   287          {entry.task_type}
   288        </TableCell>
   289        <TableCell className={clsx(isLastRow && classes.noBorder)}>
   290          <SyntaxHighlighter language="json">
   291            {prettifyPayload(entry.task_payload)}
   292          </SyntaxHighlighter>
   293        </TableCell>
   294        <TableCell className={clsx(isLastRow && classes.noBorder)}>
   295          <SyntaxHighlighter language="go">
   296            {entry.options.length > 0 ? entry.options.join(", ") : "No options"}
   297          </SyntaxHighlighter>
   298        </TableCell>
   299        <TableCell className={clsx(isLastRow && classes.noBorder)}>
   300          {durationBefore(entry.next_enqueue_at)}
   301        </TableCell>
   302        <TableCell className={clsx(isLastRow && classes.noBorder)}>
   303          {entry.prev_enqueue_at ? timeAgo(entry.prev_enqueue_at) : "N/A"}
   304        </TableCell>
   305        <TableCell>
   306          <Tooltip title="See History">
   307            <IconButton
   308              aria-label="expand row"
   309              size="small"
   310              onClick={props.onShowHistoryClick}
   311            >
   312              <HistoryIcon />
   313            </IconButton>
   314          </Tooltip>
   315        </TableCell>
   316      </TableRow>
   317    );
   318  }