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

     1  import React, { useState } from "react";
     2  import { Link } from "react-router-dom";
     3  import clsx from "clsx";
     4  import { makeStyles } from "@material-ui/core/styles";
     5  import Grid from "@material-ui/core/Grid";
     6  import Box from "@material-ui/core/Box";
     7  import Collapse from "@material-ui/core/Collapse";
     8  import IconButton from "@material-ui/core/IconButton";
     9  import Table from "@material-ui/core/Table";
    10  import TableBody from "@material-ui/core/TableBody";
    11  import TableCell from "@material-ui/core/TableCell";
    12  import TableContainer from "@material-ui/core/TableContainer";
    13  import TableHead from "@material-ui/core/TableHead";
    14  import TableRow from "@material-ui/core/TableRow";
    15  import TableSortLabel from "@material-ui/core/TableSortLabel";
    16  import Tooltip from "@material-ui/core/Tooltip";
    17  import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown";
    18  import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp";
    19  import Alert from "@material-ui/lab/Alert";
    20  import AlertTitle from "@material-ui/lab/AlertTitle";
    21  import SyntaxHighlighter from "./SyntaxHighlighter";
    22  import { ServerInfo } from "../api";
    23  import { SortDirection, SortableTableColumn } from "../types/table";
    24  import { timeAgo, uuidPrefix, prettifyPayload } from "../utils";
    25  import { queueDetailsPath } from "../paths";
    26  import Typography from "@material-ui/core/Typography";
    27  
    28  const useStyles = makeStyles((theme) => ({
    29    table: {
    30      minWidth: 650,
    31    },
    32    fixedCell: {
    33      position: "sticky",
    34      zIndex: 1,
    35      left: 0,
    36      background: theme.palette.background.paper,
    37    },
    38  }));
    39  
    40  enum SortBy {
    41    HostPID,
    42    Status,
    43    ActiveWorkers,
    44    Queues,
    45    Started,
    46  }
    47  const colConfigs: SortableTableColumn<SortBy>[] = [
    48    {
    49      label: "Host:PID",
    50      key: "host",
    51      sortBy: SortBy.HostPID,
    52      align: "left",
    53    },
    54    {
    55      label: "Started",
    56      key: "started",
    57      sortBy: SortBy.Started,
    58      align: "left",
    59    },
    60    {
    61      label: "Status",
    62      key: "status",
    63      sortBy: SortBy.Status,
    64      align: "left",
    65    },
    66    {
    67      label: "Queues",
    68      key: "queues",
    69      sortBy: SortBy.Queues,
    70      align: "left",
    71    },
    72    {
    73      label: "Active Workers",
    74      key: "workers",
    75      sortBy: SortBy.ActiveWorkers,
    76      align: "left",
    77    },
    78  ];
    79  
    80  // sortServers takes a array of server-infos and return a sorted array.
    81  // It returns a new array and leave the original array untouched.
    82  function sortServerInfos(
    83    entries: ServerInfo[],
    84    cmpFn: (first: ServerInfo, second: ServerInfo) => number
    85  ): ServerInfo[] {
    86    let copy = [...entries];
    87    copy.sort(cmpFn);
    88    return copy;
    89  }
    90  
    91  interface Props {
    92    servers: ServerInfo[];
    93  }
    94  
    95  export default function ServersTable(props: Props) {
    96    const classes = useStyles();
    97    const [sortBy, setSortBy] = useState<SortBy>(SortBy.HostPID);
    98    const [sortDir, setSortDir] = useState<SortDirection>(SortDirection.Asc);
    99  
   100    const createSortClickHandler = (sortKey: SortBy) => (e: React.MouseEvent) => {
   101      if (sortKey === sortBy) {
   102        // Toggle sort direction.
   103        const nextSortDir =
   104          sortDir === SortDirection.Asc ? SortDirection.Desc : SortDirection.Asc;
   105        setSortDir(nextSortDir);
   106      } else {
   107        // Change the sort key.
   108        setSortBy(sortKey);
   109      }
   110    };
   111  
   112    const cmpFunc = (s1: ServerInfo, s2: ServerInfo): number => {
   113      let isS1Smaller = false;
   114      switch (sortBy) {
   115        case SortBy.HostPID:
   116          if (s1.host === s2.host && s1.pid === s2.pid) return 0;
   117          if (s1.host === s2.host) {
   118            isS1Smaller = s1.pid < s2.pid;
   119          } else {
   120            isS1Smaller = s1.host < s2.host;
   121          }
   122          break;
   123        case SortBy.Started:
   124          const s1StartTime = Date.parse(s1.start_time);
   125          const s2StartTime = Date.parse(s2.start_time);
   126          if (s1StartTime === s2StartTime) return 0;
   127          isS1Smaller = s1StartTime < s2StartTime;
   128          break;
   129        case SortBy.Status:
   130          if (s1.status === s2.status) return 0;
   131          isS1Smaller = s1.status < s2.status;
   132          break;
   133        case SortBy.Queues:
   134          const s1Queues = Object.keys(s1.queue_priorities).join(",");
   135          const s2Queues = Object.keys(s2.queue_priorities).join(",");
   136          if (s1Queues === s2Queues) return 0;
   137          isS1Smaller = s1Queues < s2Queues;
   138          break;
   139        case SortBy.ActiveWorkers:
   140          if (s1.active_workers.length === s2.active_workers.length) {
   141            return 0;
   142          }
   143          isS1Smaller = s1.active_workers.length < s2.active_workers.length;
   144          break;
   145        default:
   146          // eslint-disable-next-line no-throw-literal
   147          throw `Unexpected order by value: ${sortBy}`;
   148      }
   149      if (sortDir === SortDirection.Asc) {
   150        return isS1Smaller ? -1 : 1;
   151      } else {
   152        return isS1Smaller ? 1 : -1;
   153      }
   154    };
   155  
   156    if (props.servers.length === 0) {
   157      return (
   158        <Alert severity="info">
   159          <AlertTitle>Info</AlertTitle>
   160          No servers found at this time.
   161        </Alert>
   162      );
   163    }
   164  
   165    return (
   166      <TableContainer>
   167        <Table className={classes.table} aria-label="server info table">
   168          <TableHead>
   169            <TableRow>
   170              {colConfigs.map((cfg, i) => (
   171                <TableCell
   172                  key={cfg.key}
   173                  align={cfg.align}
   174                  className={clsx(i === 0 && classes.fixedCell)}
   175                >
   176                  <TableSortLabel
   177                    active={cfg.sortBy === sortBy}
   178                    direction={sortDir}
   179                    onClick={createSortClickHandler(cfg.sortBy)}
   180                  >
   181                    {cfg.label}
   182                  </TableSortLabel>
   183                </TableCell>
   184              ))}
   185              <TableCell />
   186            </TableRow>
   187          </TableHead>
   188          <TableBody>
   189            {sortServerInfos(props.servers, cmpFunc).map((srv) => (
   190              <Row key={srv.id} server={srv} />
   191            ))}
   192          </TableBody>
   193        </Table>
   194      </TableContainer>
   195    );
   196  }
   197  interface RowProps {
   198    server: ServerInfo;
   199  }
   200  
   201  const useRowStyles = makeStyles((theme) => ({
   202    rowRoot: {
   203      "& > *": {
   204        borderBottom: "unset",
   205      },
   206    },
   207    noBorder: {
   208      border: "none",
   209    },
   210    link: {
   211      color: theme.palette.text.primary,
   212    },
   213  }));
   214  
   215  function Row(props: RowProps) {
   216    const classes = useRowStyles();
   217    const { server } = props;
   218    const [open, setOpen] = useState<boolean>(false);
   219    const qnames = Object.keys(server.queue_priorities);
   220    return (
   221      <React.Fragment>
   222        <TableRow className={classes.rowRoot}>
   223          <TableCell>
   224            {server.host}:{server.pid}
   225          </TableCell>
   226          <TableCell>{timeAgo(server.start_time)}</TableCell>
   227          <TableCell>{server.status}</TableCell>
   228          <TableCell>
   229            {qnames.map((qname, idx) => (
   230              <span key={qname}>
   231                <Link to={queueDetailsPath(qname)} className={classes.link}>
   232                  {qname}
   233                </Link>
   234                {idx === qnames.length - 1 ? "" : ", "}
   235              </span>
   236            ))}
   237          </TableCell>
   238          <TableCell>
   239            {server.active_workers.length}/{server.concurrency}
   240          </TableCell>
   241          <TableCell>
   242            <Tooltip title={open ? "Hide Details" : "Show Details"}>
   243              <IconButton
   244                aria-label="expand row"
   245                size="small"
   246                onClick={() => setOpen(!open)}
   247              >
   248                {open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
   249              </IconButton>
   250            </Tooltip>
   251          </TableCell>
   252        </TableRow>
   253        <TableRow className={classes.rowRoot}>
   254          <TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
   255            <Collapse in={open} timeout="auto" unmountOnExit>
   256              <Grid container spacing={2}>
   257                <Grid item xs={9}>
   258                  <Typography
   259                    variant="subtitle1"
   260                    gutterBottom
   261                    color="textSecondary"
   262                  >
   263                    Active Workers
   264                  </Typography>
   265                  <Table size="small" aria-label="active workers">
   266                    <TableHead>
   267                      <TableRow>
   268                        <TableCell>Task ID</TableCell>
   269                        <TableCell>Task Payload</TableCell>
   270                        <TableCell>Queue</TableCell>
   271                        <TableCell>Started</TableCell>
   272                      </TableRow>
   273                    </TableHead>
   274                    <TableBody>
   275                      {server.active_workers.map((worker) => (
   276                        <TableRow key={worker.task_id}>
   277                          <TableCell component="th" scope="row">
   278                            {uuidPrefix(worker.task_id)}
   279                          </TableCell>
   280                          <TableCell>
   281                            <SyntaxHighlighter
   282                              language="json"
   283                              customStyle={{ margin: 0 }}
   284                            >
   285                              {prettifyPayload(worker.task_payload)}
   286                            </SyntaxHighlighter>
   287                          </TableCell>
   288                          <TableCell>{worker.queue}</TableCell>
   289                          <TableCell>{timeAgo(worker.start_time)}</TableCell>
   290                        </TableRow>
   291                      ))}
   292                    </TableBody>
   293                  </Table>
   294                </Grid>
   295                <Grid item xs={3}>
   296                  <Typography
   297                    variant="subtitle1"
   298                    gutterBottom
   299                    color="textSecondary"
   300                  >
   301                    Queue Priority
   302                  </Typography>
   303                  <Table size="small" aria-label="active workers">
   304                    <TableHead>
   305                      <TableRow>
   306                        <TableCell>Queue</TableCell>
   307                        <TableCell align="right">Priority</TableCell>
   308                      </TableRow>
   309                    </TableHead>
   310                    <TableBody>
   311                      {qnames.map((qname) => (
   312                        <TableRow key={qname}>
   313                          <TableCell>
   314                            <Link
   315                              to={queueDetailsPath(qname)}
   316                              className={classes.link}
   317                            >
   318                              {qname}
   319                            </Link>
   320                          </TableCell>
   321                          <TableCell align="right">
   322                            {server.queue_priorities[qname]}
   323                          </TableCell>
   324                        </TableRow>
   325                      ))}
   326                    </TableBody>
   327                  </Table>
   328                  <Box margin={2}>
   329                    <Typography variant="subtitle2" component="span">
   330                      Strict Priority:{" "}
   331                    </Typography>
   332                    <Typography variant="button" component="span">
   333                      {server.strict_priority_enabled ? "ON" : "OFF"}
   334                    </Typography>
   335                  </Box>
   336                </Grid>
   337              </Grid>
   338            </Collapse>
   339          </TableCell>
   340        </TableRow>
   341      </React.Fragment>
   342    );
   343  }