github.com/grafana/pyroscope@v1.18.0/public/app/ui/Table.tsx (about)

     1  import React, { useState, ReactNode, CSSProperties, RefObject } from 'react';
     2  import { faChevronLeft } from '@fortawesome/free-solid-svg-icons/faChevronLeft';
     3  import { faChevronRight } from '@fortawesome/free-solid-svg-icons/faChevronRight';
     4  import clsx from 'clsx';
     5  
     6  import styles from './Table.module.scss';
     7  import LoadingSpinner from './LoadingSpinner';
     8  import Button from './Button';
     9  
    10  interface CustomProp {
    11    [k: string]: string | CSSProperties | ReactNode | number | undefined;
    12  }
    13  
    14  export interface Cell extends CustomProp {
    15    value: ReactNode | string;
    16    style?: CSSProperties;
    17  }
    18  
    19  interface HeadCell extends CustomProp {
    20    name: string;
    21    label: string;
    22    sortable?: number;
    23    default?: boolean;
    24  }
    25  
    26  export interface BodyRow {
    27    'data-row'?: string;
    28    isRowSelected?: boolean;
    29    isRowDisabled?: boolean;
    30    cells: Cell[];
    31    onClick?: () => void;
    32    className?: string;
    33  }
    34  
    35  export type TableBodyType =
    36    | {
    37        type: 'not-filled';
    38        value: string | ReactNode;
    39        bodyClassName?: string;
    40      }
    41    | {
    42        type: 'filled';
    43        bodyRows: BodyRow[];
    44      };
    45  
    46  type Table = TableBodyType & {
    47    headRow: HeadCell[];
    48  };
    49  
    50  interface TableSortProps {
    51    sortBy: string;
    52    updateSortParams: (v: string) => void;
    53    sortByDirection: 'desc' | 'asc';
    54  }
    55  
    56  export const useTableSort = (headRow: HeadCell[]): TableSortProps => {
    57    const defaultSortByCell =
    58      headRow.filter((row) => row?.default)[0] || headRow[0];
    59    const [sortBy, updateSortBy] = useState(defaultSortByCell.name);
    60    const [sortByDirection, setSortByDirection] = useState<'desc' | 'asc'>(
    61      'desc'
    62    );
    63  
    64    const updateSortParams = (newSortBy: string) => {
    65      let dir = sortByDirection;
    66  
    67      if (sortBy === newSortBy) {
    68        dir = dir === 'asc' ? 'desc' : 'asc';
    69      } else {
    70        dir = 'desc';
    71      }
    72  
    73      updateSortBy(newSortBy);
    74      setSortByDirection(dir);
    75    };
    76  
    77    return { sortBy, sortByDirection, updateSortParams };
    78  };
    79  
    80  interface TableProps {
    81    sortByDirection?: string;
    82    sortBy?: string;
    83    updateSortParams?: (newSortBy: string) => void;
    84    table: Table;
    85    tableBodyRef?: RefObject<HTMLTableSectionElement>;
    86    className?: string;
    87    isLoading?: boolean;
    88    /* enables pagination */
    89    itemsPerPage?: number;
    90    tableStyle?: React.CSSProperties;
    91  }
    92  
    93  function TableComponent({
    94    sortByDirection,
    95    sortBy,
    96    updateSortParams,
    97    table,
    98    tableBodyRef,
    99    className,
   100    isLoading,
   101    itemsPerPage,
   102    tableStyle,
   103  }: TableProps) {
   104    const hasSort = sortByDirection && sortBy && updateSortParams;
   105    const [currPage, setCurrPage] = useState(0);
   106  
   107    return isLoading ? (
   108      <div className={styles.loadingSpinner}>
   109        <LoadingSpinner />
   110      </div>
   111    ) : (
   112      <>
   113        <table
   114          className={clsx(styles.table, {
   115            [className || '']: className,
   116          })}
   117          data-testid="table-ui"
   118          style={tableStyle}
   119        >
   120          <thead>
   121            <tr>
   122              {table.headRow.map(
   123                ({ sortable, label, name, ...rest }, idx: number) =>
   124                  !sortable || table.type === 'not-filled' || !hasSort ? (
   125                    // eslint-disable-next-line react/no-array-index-key
   126                    <th key={idx} {...rest}>
   127                      {label}
   128                    </th>
   129                  ) : (
   130                    <th
   131                      {...rest}
   132                      // eslint-disable-next-line react/no-array-index-key
   133                      key={idx}
   134                      className={styles.sortable}
   135                      onClick={() => updateSortParams(name)}
   136                    >
   137                      {label}
   138                      <span
   139                        className={clsx(styles.sortArrow, {
   140                          [styles[sortByDirection]]: sortBy === name,
   141                        })}
   142                      />
   143                    </th>
   144                  )
   145              )}
   146            </tr>
   147          </thead>
   148          <tbody ref={tableBodyRef}>
   149            {table.type === 'not-filled' ? (
   150              <tr className={table?.bodyClassName}>
   151                <td colSpan={table.headRow.length}>{table.value}</td>
   152              </tr>
   153            ) : (
   154              paginate(table.bodyRows, currPage, itemsPerPage).map(
   155                ({ cells, isRowSelected, isRowDisabled, className, ...rest }) => {
   156                  // The problem is that when you switch apps or time-range and the function
   157                  // names stay the same it leads to an issue where rows don't get re-rendered
   158                  // So we force a rerender each time.
   159                  const renderID = Math.random();
   160  
   161                  return (
   162                    <tr
   163                      key={renderID}
   164                      {...rest}
   165                      className={clsx(className, {
   166                        [styles.isRowSelected]: isRowSelected,
   167                        [styles.isRowDisabled]: isRowDisabled,
   168                      })}
   169                    >
   170                      {cells &&
   171                        cells.map(
   172                          ({ style, value, ...rest }: Cell, index: number) => (
   173                            // eslint-disable-next-line react/no-array-index-key
   174                            <td key={renderID + index} style={style} {...rest}>
   175                              {value}
   176                            </td>
   177                          )
   178                        )}
   179                    </tr>
   180                  );
   181                }
   182              )
   183            )}
   184          </tbody>
   185        </table>
   186        <PaginationNavigation
   187          bodyRows={table.type === 'filled' ? table.bodyRows : undefined}
   188          itemsPerPage={itemsPerPage}
   189          currPage={currPage}
   190          setCurrPage={setCurrPage}
   191        />
   192      </>
   193    );
   194  }
   195  
   196  function paginate(
   197    bodyRows: Extract<Table, { type: 'filled' }>['bodyRows'],
   198    currPage: number,
   199    itemsPerPage?: TableProps['itemsPerPage']
   200  ) {
   201    if (!itemsPerPage) {
   202      return bodyRows;
   203    }
   204  
   205    return bodyRows.slice(currPage * itemsPerPage, itemsPerPage * (currPage + 1));
   206  }
   207  
   208  interface PaginationNavigationProps {
   209    bodyRows?: Extract<Table, { type: 'filled' }>['bodyRows'];
   210    currPage: number;
   211    itemsPerPage?: TableProps['itemsPerPage'];
   212    setCurrPage: (i: number) => void;
   213  }
   214  
   215  function PaginationNavigation({
   216    itemsPerPage,
   217    currPage,
   218    setCurrPage,
   219    bodyRows,
   220  }: PaginationNavigationProps) {
   221    if (!itemsPerPage) {
   222      return null;
   223    }
   224  
   225    const isThereNextPage = bodyRows
   226      ? paginate(bodyRows, currPage + 1, itemsPerPage).length > 0
   227      : false;
   228  
   229    const isTherePreviousPage = bodyRows
   230      ? paginate(bodyRows, currPage - 1, itemsPerPage).length > 0
   231      : false;
   232  
   233    return (
   234      <nav className={styles.pagination}>
   235        <Button
   236          aria-label="Previous Page"
   237          disabled={!isTherePreviousPage}
   238          kind="float"
   239          icon={faChevronLeft}
   240          onClick={() => setCurrPage(currPage - 1)}
   241        />
   242        <Button
   243          disabled={!isThereNextPage}
   244          aria-label="Next Page"
   245          kind="float"
   246          icon={faChevronRight}
   247          onClick={() => setCurrPage(currPage + 1)}
   248        />
   249      </nav>
   250    );
   251  }
   252  
   253  export default TableComponent;