livepeer/livepeerjs

View on GitHub
packages/explorer-2.0/components/Orchestrators/PerformanceTable.tsx

Summary

Maintainability
F
1 wk
Test Coverage
import Box from "../Box";
import Flex from "../Flex";
import { forwardRef, useMemo } from "react";
import { useTable, useFilters, useSortBy, usePagination } from "react-table";
import Help from "../../public/img/help.svg";
import matchSorter from "match-sorter";
import AccountCell from "../AccountCell";
import ReactTooltip from "react-tooltip";
import Link from "next/link";
import { TableCellProps } from "../../@types";
import {
  ChevronDownIcon,
  ChevronUpIcon,
  ArrowLeftIcon,
  ArrowRightIcon,
  MagnifyingGlassIcon,
} from "@modulz/radix-icons";

const PerformanceTable = ({
  pageSize = 10,
  data: { currentRound, transcoders },
  region,
}) => {
  function fuzzyTextFilterFn(rows, id, filterValue) {
    return matchSorter(rows, filterValue, {
      keys: [(row) => row.values[id]],
    });
  }

  // Let the table remove the filter if the string is empty
  fuzzyTextFilterFn.autoRemove = (val) => !val;

  function DefaultColumnFilter({ column: { filterValue, setFilter } }) {
    return (
      <Flex
        css={{
          alignItems: "center",
          pl: "$4",
        }}>
        <Box
          as={MagnifyingGlassIcon}
          css={{ width: 16, height: 16, mr: "$2", color: "$muted" }}
        />
        <Box
          as="input"
          value={filterValue || ""}
          onChange={(e) => {
            setFilter(e.target.value || undefined);
          }}
          placeholder={`Filter`}
          type="text"
          css={{
            display: "block",
            outline: "none",
            width: "100%",
            appearance: "none",
            fontSize: "$3",
            lineHeight: "inherit",
            border: 0,
            color: "inherit",
            bg: "transparent",
          }}
        />
      </Flex>
    );
  }

  const columns: any = useMemo(
    () => [
      {
        Header: "#",
        accessor: "rank",
        disableSortBy: true,
      },
      {
        Header: "Orchestrator",
        accessor: "id",
        filter: "fuzzyText",
        Filter: DefaultColumnFilter,
        mobile: true,
        sortType: (rowA, rowB, columnID) => {
          const rowAIdentity =
            getRowValueByColumnID(rowA, "threeBoxSpace")?.name ||
            getRowValueByColumnID(rowA, columnID);
          const rowBIdentity =
            getRowValueByColumnID(rowB, "threeBoxSpace")?.name ||
            getRowValueByColumnID(rowB, columnID);
          return compareBasic(rowAIdentity, rowBIdentity);
        },
      },
      {
        Header: "Activation Round",
        accessor: "activationRound",
      },
      {
        Header: "Deactivation Round",
        accessor: "deactivationRound",
      },
      {
        Header: "ThreeBoxSpace",
        accessor: "threeBoxSpace",
        filter: "fuzzyText",
        Filter: DefaultColumnFilter,
      },
      {
        Header: "Delegator",
        accessor: "delegator",
      },
      {
        Header: "Total Score (0-10)",
        accessor: `scores.${region}`,
        mobile: true,
        sortDescFirst: true,
        defaultCanSort: true,
      },
      {
        Header: "Success Rate (%)",
        accessor: `successRates.${region}`,
        mobile: false,
      },
      {
        Header: "Latency Score (0-10)",
        accessor: `roundTripScores.${region}`,
        mobile: false,
      },
    ],
    [region]
  );

  function getRowValueByColumnID(row, columnID) {
    return row.values[columnID];
  }

  function compareBasic(a, b) {
    return a === b ? 0 : a > b ? 1 : -1;
  }

  const defaultColumn = useMemo(
    () => ({
      // Let's set up our default Filter UI
      Filter: DefaultColumnFilter,
    }),
    []
  );

  const filterTypes = useMemo(
    () => ({
      // Add a new fuzzyTextFilterFn filter type.
      fuzzyText: fuzzyTextFilterFn,
      // Or, override the default text filter to use
      // "startWith"
      text: (rows, id, filterValue) => {
        return rows.filter((row) => {
          const rowValue = row.values[id];
          return rowValue !== undefined
            ? String(rowValue)
                .toLowerCase()
                .startsWith(String(filterValue).toLowerCase())
            : true;
        });
      },
    }),
    []
  );

  const tableOptions: any = {
    columns,
    data: transcoders,
    disableSortRemove: true,
    autoResetPage: true,
    initialState: {
      pageSize,
      sortBy: [
        {
          id: "scores.global",
          desc: true,
        },
        {
          id: "scores.fra",
          desc: true,
        },
        {
          id: "scores.lax",
          desc: true,
        },
        {
          id: "scores.lon",
          desc: true,
        },
        {
          id: "scores.mdw",
          desc: true,
        },
        {
          id: "scores.nyc",
          desc: true,
        },
        {
          id: "scores.prg",
          desc: true,
        },
        {
          id: "scores.sin",
          desc: true,
        },
      ],
      hiddenColumns: [
        "activationRound",
        "deactivationRound",
        "threeBoxSpace",
        "global",
        "delegator",
      ],
    },
    defaultColumn,
    filterTypes,
  };

  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    prepareRow,
    page,
    canPreviousPage,
    canNextPage,
    pageCount,
    nextPage,
    previousPage,
    state: { pageIndex },
  }: any = useTable(tableOptions, useFilters, useSortBy, usePagination);

  const accountColumn = headerGroups[0].headers[1];

  return (
    <>
      <Flex
        css={{
          position: "relative",
          width: "100%",
          top: 0,
          zIndex: 10,
          flexDirection: "column",
          alignItems: "flex-start",
          pt: 0,
          pb: "$4",
          ml: 0,
          mr: 0,
          justifyContent: "space-between",
        }}>
        <Box>{accountColumn.render("Filter")}</Box>
      </Flex>
      <Box css={{ overflow: "scroll", WebkitOverflowScrolling: "touch" }}>
        <Box
          css={{
            display: "table",
            tableLayout: "fixed",
            width: "100%",
            minWidth: 780,
            borderSpacing: "0",
            borderCollapse: "collapse",
          }}
          {...getTableProps()}>
          <Box css={{ display: "table-header-group" }}>
            {headerGroups.map((headerGroup, index1) => (
              <Box
                css={{ display: "table-row" }}
                key={index1}
                {...headerGroup.getHeaderGroupProps()}>
                {headerGroup.headers.map((column, index2) => (
                  <Box
                    css={{
                      borderBottom: "1px solid",
                      borderColor: "rgba(255,255,255,.05)",
                      pb: "$2",
                      pl: "$4",
                      pr: index2 === 0 ? 0 : "$3",
                      width: index2 === 0 ? 30 : "auto",
                      fontWeight: 700,
                      display: "table-cell",
                      textTransform: "uppercase",
                    }}
                    key={index2}>
                    <Flex
                      css={{
                        justifyContent:
                          index2 === 0 || index2 === 1
                            ? "flex-start"
                            : "flex-end",
                      }}>
                      <Flex
                        css={{
                          alignItems: "center",
                          fontSize: 10,
                          position: "relative",
                        }}
                        {...column.getHeaderProps(
                          column.getSortByToggleProps({ title: "" })
                        )}>
                        {column.isSorted ? (
                          column.isSortedDesc ? (
                            <Box
                              as={ChevronDownIcon}
                              css={{ height: 15, mr: "$2" }}
                            />
                          ) : (
                            <Box
                              as={ChevronUpIcon}
                              css={{ height: 15, mr: "$2" }}
                            />
                          )
                        ) : (
                          <Box css={{ height: 15 }} />
                        )}
                        {column.render("Header")}
                      </Flex>
                      {renderTooltip(column.render("Header"))}
                    </Flex>
                  </Box>
                ))}
              </Box>
            ))}
          </Box>

          <Box css={{ display: "table-row-group" }} {...getTableBodyProps()}>
            {page.map((row, rowIndex) => {
              const orchestratorIndex = rowIndex + pageIndex * pageSize;
              prepareRow(row);

              return (
                <Box
                  {...row.getRowProps()}
                  key={orchestratorIndex}
                  css={{
                    display: "table-row",
                    height: 64,
                  }}>
                  {row.cells.map((cell, i) => {
                    switch (cell.column.Header) {
                      case "#":
                        return (
                          <TableCell cell={cell} key={i} css={{ pr: 0 }}>
                            {parseInt(rowIndex) + 1 + pageIndex * pageSize}
                          </TableCell>
                        );
                      case "Orchestrator":
                        const active =
                          cell.row.values.activationRound <= currentRound.id &&
                          cell.row.values.deactivationRound > currentRound.id;
                        return (
                          <TableCell cell={cell} key={i} css={{ pr: 0 }}>
                            <Link
                              href={`/accounts/${cell.value}/campaign`}
                              passHref>
                              <Box
                                as="a"
                                css={{
                                  display: "inherit",
                                  color: "inherit",
                                  ":hover": {
                                    textDecoration: "underline",
                                  },
                                }}>
                                <AccountCell
                                  active={active}
                                  threeBoxSpace={cell.row.values.threeBoxSpace}
                                  address={cell.value}
                                />
                              </Box>
                            </Link>
                          </TableCell>
                        );
                      case "Success Rate (%)":
                        if (
                          typeof cell.value === "undefined" ||
                          cell.value === null
                        ) {
                          return <TableCell cell={cell} key={i} />;
                        }
                        return (
                          <TableCell
                            cell={cell}
                            key={i}
                            css={{
                              fontFamily: "$monospace",
                              textAlign: "right",
                            }}>
                            {cell.value.toFixed(2)}%
                          </TableCell>
                        );
                      case "Latency Score (0-10)":
                        if (
                          typeof cell.value === "undefined" ||
                          cell.value === null
                        ) {
                          return <TableCell cell={cell} key={i} />;
                        }

                        return (
                          <TableCell
                            cell={cell}
                            key={i}
                            css={{
                              fontFamily: "$monospace",
                              textAlign: "right",
                            }}>
                            {(cell.value / 1000).toFixed(2)}
                          </TableCell>
                        );
                      case "Total Score (0-10)":
                        if (
                          typeof cell.value === "undefined" ||
                          cell.value === null
                        ) {
                          return <TableCell cell={cell} key={i} />;
                        }

                        return (
                          <TableCell
                            cell={cell}
                            key={i}
                            css={{
                              fontFamily: "$monospace",
                              textAlign: "right",
                            }}>
                            {(cell.value / 1000).toFixed(2)}
                          </TableCell>
                        );
                      default:
                        return <TableCell cell={cell} key={i} />;
                    }
                  })}
                </Box>
              );
            })}
          </Box>
        </Box>
      </Box>
      <Flex
        css={{
          py: "$4",
          alignItems: "center",
          justifyContent: "center",
        }}>
        <Box
          as={ArrowLeftIcon}
          css={{
            cursor: "pointer",
            color: canPreviousPage ? "$primary" : "$text",
            opacity: canPreviousPage ? 1 : 0.5,
          }}
          onClick={() => {
            if (canPreviousPage) {
              previousPage();
            }
          }}
        />
        <Flex css={{ alignItems: "center", fontSize: "$2", mx: "$3" }}>
          <Box css={{ mr: "$1" }}>Page</Box>
          <Box as="span" css={{ fontFamily: "$monospace" }}>
            {pageIndex + 1}
          </Box>
          <Box css={{ mx: "$1" }}>of</Box>
          <Box css={{ fontFamily: "$monospace" }}>{pageCount}</Box>
        </Flex>
        <Box
          as={ArrowRightIcon}
          css={{
            cursor: "pointer",
            color: canNextPage ? "$primary" : "$text",
            opacity: canNextPage ? 1 : 0.5,
          }}
          onClick={() => {
            if (canNextPage) {
              nextPage();
            }
          }}
        />
      </Flex>
    </>
  );
  function renderTooltip(title) {
    switch (title) {
      case "Success Rate (%)":
        return (
          <>
            <ReactTooltip
              html={true}
              id="tooltip-success-rate"
              className="tooltip"
              place="bottom"
              type="dark"
              effect="solid"
              delayHide={200}
              delayUpdate={500}
            />
            <Box
              as={Help}
              data-tip='<span>The average percentage of video segments sent by a broadcaster that are successfully transcoded. See <a href="https://livepeer.org/docs/video-miners/reference/leaderboard" rel="noopener noreferrer" target="_blank">the FAQ</a> for more details on how this metric is calculated.</span>'
              data-for="tooltip-success-rate"
              css={{
                cursor: "pointer",
                position: "relative",
                ml: "$2",
                top: "1px",
                width: 12,
                height: 12,
              }}
            />
          </>
        );
      case "Latency Score (0-10)":
        return (
          <>
            <ReactTooltip
              html={true}
              id="tooltip-latency-score"
              className="tooltip"
              place="bottom"
              type="dark"
              effect="solid"
              delayHide={200}
              delayUpdate={500}
            />
            <Box
              as={Help}
              data-tip='<span>The average utility of the overall transcoding latency for an orchestrator. See <a href="https://livepeer.org/docs/video-miners/reference/leaderboard" rel="noopener noreferrer" target="_blank">the FAQ</a> for more details on how this metric is calculated.</span>'
              data-for="tooltip-latency-score"
              css={{
                cursor: "pointer",
                position: "relative",
                ml: "$2",
                top: "1px",
                width: 12,
                height: 12,
              }}
            />
          </>
        );
      case "Total Score (0-10)":
        return (
          <>
            <ReactTooltip
              html={true}
              id="tooltip-score"
              className="tooltip"
              place="bottom"
              type="dark"
              effect="solid"
              delayHide={200}
              delayUpdate={500}
            />
            <Box
              as={Help}
              data-tip='<span>The average utility of the overall quality and reliability of an orchestrator based on success rate and latency scores. See <a href="https://livepeer.org/docs/video-miners/reference/leaderboard" rel="noopener noreferrer" target="_blank">the FAQ</a> for more details on how this metric is calculated.</span>'
              data-for="tooltip-score"
              css={{
                cursor: "pointer",
                position: "relative",
                ml: "$2",
                top: "1px",
                width: 12,
                height: 12,
              }}
            />
          </>
        );

      default:
        return null;
    }
  }
};

const TableCell = forwardRef(
  (
    { children, href, target, cell, onClick, as = "div", css }: TableCellProps,
    ref
  ) => {
    return (
      <Box
        as={as}
        target={target}
        href={href}
        ref={ref}
        onClick={onClick}
        css={{
          justifyContent: "flex-end",
          color: "inherit",
          display: "table-cell",
          width: "auto",
          fontSize: "$2",
          px: "$4",
          verticalAlign: "middle",
          borderBottom: "1px solid",
          borderColor: "rgba(255,255,255,.05)",
          ...css,
        }}
        {...cell.getCellProps()}>
        {children}
      </Box>
    );
  }
);

export default PerformanceTable;