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

     1  import React, { useState } from "react";
     2  import clsx from "clsx";
     3  import { Link } from "react-router-dom";
     4  import { makeStyles } from "@material-ui/core/styles";
     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 TableSortLabel from "@material-ui/core/TableSortLabel";
    12  import IconButton from "@material-ui/core/IconButton";
    13  import Tooltip from "@material-ui/core/Tooltip";
    14  import PauseCircleFilledIcon from "@material-ui/icons/PauseCircleFilled";
    15  import PlayCircleFilledIcon from "@material-ui/icons/PlayCircleFilled";
    16  import DeleteIcon from "@material-ui/icons/Delete";
    17  import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
    18  import DeleteQueueConfirmationDialog from "./DeleteQueueConfirmationDialog";
    19  import { Queue } from "../api";
    20  import { queueDetailsPath } from "../paths";
    21  import { SortDirection, SortableTableColumn } from "../types/table";
    22  import prettyBytes from "pretty-bytes";
    23  import { percentage } from "../utils";
    24  
    25  const useStyles = makeStyles((theme) => ({
    26    table: {
    27      minWidth: 650,
    28    },
    29    fixedCell: {
    30      position: "sticky",
    31      zIndex: 1,
    32      left: 0,
    33      background: theme.palette.background.paper,
    34    },
    35  }));
    36  
    37  interface QueueWithMetadata extends Queue {
    38    requestPending: boolean; // indicates pause/resume/delete request is pending for the queue.
    39  }
    40  
    41  interface Props {
    42    queues: QueueWithMetadata[];
    43    onPauseClick: (qname: string) => Promise<void>;
    44    onResumeClick: (qname: string) => Promise<void>;
    45    onDeleteClick: (qname: string) => Promise<void>;
    46  }
    47  
    48  enum SortBy {
    49    Queue,
    50    State,
    51    Size,
    52    MemoryUsage,
    53    Latency,
    54    Processed,
    55    Failed,
    56    ErrorRate,
    57  
    58    None, // no sort support
    59  }
    60  
    61  const colConfigs: SortableTableColumn<SortBy>[] = [
    62    { label: "Queue", key: "queue", sortBy: SortBy.Queue, align: "left" },
    63    { label: "State", key: "state", sortBy: SortBy.State, align: "left" },
    64    {
    65      label: "Size",
    66      key: "size",
    67      sortBy: SortBy.Size,
    68      align: "right",
    69    },
    70    {
    71      label: "Memory usage",
    72      key: "memory_usage",
    73      sortBy: SortBy.MemoryUsage,
    74      align: "right",
    75    },
    76    {
    77      label: "Latency",
    78      key: "latency",
    79      sortBy: SortBy.Latency,
    80      align: "right",
    81    },
    82    {
    83      label: "Processed",
    84      key: "processed",
    85      sortBy: SortBy.Processed,
    86      align: "right",
    87    },
    88    { label: "Failed", key: "failed", sortBy: SortBy.Failed, align: "right" },
    89    {
    90      label: "Error rate",
    91      key: "error_rate",
    92      sortBy: SortBy.ErrorRate,
    93      align: "right",
    94    },
    95    { label: "Actions", key: "actions", sortBy: SortBy.None, align: "center" },
    96  ];
    97  
    98  // sortQueues takes a array of queues and return a sorted array.
    99  // It returns a new array and leave the original array untouched.
   100  function sortQueues(
   101    queues: QueueWithMetadata[],
   102    cmpFn: (first: QueueWithMetadata, second: QueueWithMetadata) => number
   103  ): QueueWithMetadata[] {
   104    let copy = [...queues];
   105    copy.sort(cmpFn);
   106    return copy;
   107  }
   108  
   109  export default function QueuesOverviewTable(props: Props) {
   110    const classes = useStyles();
   111    const [sortBy, setSortBy] = useState<SortBy>(SortBy.Queue);
   112    const [sortDir, setSortDir] = useState<SortDirection>(SortDirection.Asc);
   113    const [queueToDelete, setQueueToDelete] = useState<QueueWithMetadata | null>(
   114      null
   115    );
   116    const createSortClickHandler = (sortKey: SortBy) => (e: React.MouseEvent) => {
   117      if (sortKey === sortBy) {
   118        // Toggle sort direction.
   119        const nextSortDir =
   120          sortDir === SortDirection.Asc ? SortDirection.Desc : SortDirection.Asc;
   121        setSortDir(nextSortDir);
   122      } else {
   123        // Change the sort key.
   124        setSortBy(sortKey);
   125      }
   126    };
   127  
   128    const cmpFunc = (q1: QueueWithMetadata, q2: QueueWithMetadata): number => {
   129      let isQ1Smaller: boolean;
   130      switch (sortBy) {
   131        case SortBy.Queue:
   132          if (q1.queue === q2.queue) return 0;
   133          isQ1Smaller = q1.queue < q2.queue;
   134          break;
   135        case SortBy.State:
   136          if (q1.paused === q2.paused) return 0;
   137          isQ1Smaller = !q1.paused;
   138          break;
   139        case SortBy.Size:
   140          if (q1.size === q2.size) return 0;
   141          isQ1Smaller = q1.size < q2.size;
   142          break;
   143        case SortBy.MemoryUsage:
   144          if (q1.memory_usage_bytes === q2.memory_usage_bytes) return 0;
   145          isQ1Smaller = q1.memory_usage_bytes < q2.memory_usage_bytes;
   146          break;
   147        case SortBy.Latency:
   148          if (q1.latency_msec === q2.latency_msec) return 0;
   149          isQ1Smaller = q1.latency_msec < q2.latency_msec;
   150          break;
   151        case SortBy.Processed:
   152          if (q1.processed === q2.processed) return 0;
   153          isQ1Smaller = q1.processed < q2.processed;
   154          break;
   155        case SortBy.Failed:
   156          if (q1.failed === q2.failed) return 0;
   157          isQ1Smaller = q1.failed < q2.failed;
   158          break;
   159        case SortBy.ErrorRate:
   160          const q1ErrorRate = q1.failed / q1.processed;
   161          const q2ErrorRate = q2.failed / q2.processed;
   162          if (q1ErrorRate === q2ErrorRate) return 0;
   163          isQ1Smaller = q1ErrorRate < q2ErrorRate;
   164          break;
   165        default:
   166          // eslint-disable-next-line no-throw-literal
   167          throw `Unexpected order by value: ${sortBy}`;
   168      }
   169      if (sortDir === SortDirection.Asc) {
   170        return isQ1Smaller ? -1 : 1;
   171      } else {
   172        return isQ1Smaller ? 1 : -1;
   173      }
   174    };
   175  
   176    const handleDialogClose = () => {
   177      setQueueToDelete(null);
   178    };
   179  
   180    return (
   181      <React.Fragment>
   182        <TableContainer>
   183          <Table className={classes.table} aria-label="queues overview table">
   184            <TableHead>
   185              <TableRow>
   186                {colConfigs
   187                  .filter((cfg) => {
   188                    // Filter out actions column in readonly mode.
   189                    return !window.READ_ONLY || cfg.key !== "actions";
   190                  })
   191                  .map((cfg, i) => (
   192                    <TableCell
   193                      key={cfg.key}
   194                      align={cfg.align}
   195                      className={clsx(i === 0 && classes.fixedCell)}
   196                    >
   197                      {cfg.sortBy !== SortBy.None ? (
   198                        <TableSortLabel
   199                          active={sortBy === cfg.sortBy}
   200                          direction={sortDir}
   201                          onClick={createSortClickHandler(cfg.sortBy)}
   202                        >
   203                          {cfg.label}
   204                        </TableSortLabel>
   205                      ) : (
   206                        <div>{cfg.label}</div>
   207                      )}
   208                    </TableCell>
   209                  ))}
   210              </TableRow>
   211            </TableHead>
   212            <TableBody>
   213              {sortQueues(props.queues, cmpFunc).map((q) => (
   214                <Row
   215                  key={q.queue}
   216                  queue={q}
   217                  onPauseClick={() => props.onPauseClick(q.queue)}
   218                  onResumeClick={() => props.onResumeClick(q.queue)}
   219                  onDeleteClick={() => setQueueToDelete(q)}
   220                />
   221              ))}
   222            </TableBody>
   223          </Table>
   224        </TableContainer>
   225        <DeleteQueueConfirmationDialog
   226          onClose={handleDialogClose}
   227          queue={queueToDelete}
   228        />
   229      </React.Fragment>
   230    );
   231  }
   232  
   233  const useRowStyles = makeStyles((theme) => ({
   234    row: {
   235      "&:last-child td": {
   236        borderBottomWidth: 0,
   237      },
   238      "&:last-child th": {
   239        borderBottomWidth: 0,
   240      },
   241    },
   242    linkText: {
   243      textDecoration: "none",
   244      color: theme.palette.text.primary,
   245      "&:hover": {
   246        textDecoration: "underline",
   247      },
   248    },
   249    textGreen: {
   250      color: theme.palette.success.dark,
   251    },
   252    textRed: {
   253      color: theme.palette.error.dark,
   254    },
   255    boldCell: {
   256      fontWeight: 600,
   257    },
   258    fixedCell: {
   259      position: "sticky",
   260      zIndex: 1,
   261      left: 0,
   262      background: theme.palette.background.paper,
   263    },
   264    actionIconsContainer: {
   265      display: "flex",
   266      justifyContent: "center",
   267      minWidth: "100px",
   268    },
   269  }));
   270  
   271  interface RowProps {
   272    queue: QueueWithMetadata;
   273    onPauseClick: () => void;
   274    onResumeClick: () => void;
   275    onDeleteClick: () => void;
   276  }
   277  
   278  function Row(props: RowProps) {
   279    const classes = useRowStyles();
   280    const { queue: q } = props;
   281    const [showIcons, setShowIcons] = useState<boolean>(false);
   282    return (
   283      <TableRow key={q.queue} className={classes.row}>
   284        <TableCell
   285          component="th"
   286          scope="row"
   287          className={clsx(classes.boldCell, classes.fixedCell)}
   288        >
   289          <Link to={queueDetailsPath(q.queue)} className={classes.linkText}>
   290            {q.queue}
   291          </Link>
   292        </TableCell>
   293        <TableCell>
   294          {q.paused ? (
   295            <span className={classes.textRed}>paused</span>
   296          ) : (
   297            <span className={classes.textGreen}>run</span>
   298          )}
   299        </TableCell>
   300        <TableCell align="right">{q.size}</TableCell>
   301        <TableCell align="right">{prettyBytes(q.memory_usage_bytes)}</TableCell>
   302        <TableCell align="right">{q.display_latency}</TableCell>
   303        <TableCell align="right">{q.processed}</TableCell>
   304        <TableCell align="right">{q.failed}</TableCell>
   305        <TableCell align="right">{percentage(q.failed, q.processed)}</TableCell>
   306        {!window.READ_ONLY && (
   307          <TableCell
   308            align="center"
   309            onMouseEnter={() => setShowIcons(true)}
   310            onMouseLeave={() => setShowIcons(false)}
   311          >
   312            <div className={classes.actionIconsContainer}>
   313              {showIcons ? (
   314                <React.Fragment>
   315                  {q.paused ? (
   316                    <Tooltip title="Resume">
   317                      <IconButton
   318                        color="secondary"
   319                        onClick={props.onResumeClick}
   320                        disabled={q.requestPending}
   321                        size="small"
   322                      >
   323                        <PlayCircleFilledIcon fontSize="small" />
   324                      </IconButton>
   325                    </Tooltip>
   326                  ) : (
   327                    <Tooltip title="Pause">
   328                      <IconButton
   329                        color="primary"
   330                        onClick={props.onPauseClick}
   331                        disabled={q.requestPending}
   332                        size="small"
   333                      >
   334                        <PauseCircleFilledIcon fontSize="small" />
   335                      </IconButton>
   336                    </Tooltip>
   337                  )}
   338                  <Tooltip title="Delete">
   339                    <IconButton onClick={props.onDeleteClick} size="small">
   340                      <DeleteIcon fontSize="small" />
   341                    </IconButton>
   342                  </Tooltip>
   343                </React.Fragment>
   344              ) : (
   345                <IconButton size="small">
   346                  <MoreHorizIcon fontSize="small" />
   347                </IconButton>
   348              )}
   349            </div>
   350          </TableCell>
   351        )}
   352      </TableRow>
   353    );
   354  }