summaryrefslogtreecommitdiffhomepage
path: root/frontend/src/components/tables/BaseTable.tsx
blob: d378d6f56c44f92ef19725459011619bf785df3d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import { useIsLoading } from "@/contexts";
import { usePageSize } from "@/utilities/storage";
import { Box, createStyles, Skeleton, Table, Text } from "@mantine/core";
import { ReactNode, useMemo } from "react";
import { HeaderGroup, Row, TableInstance } from "react-table";

export type BaseTableProps<T extends object> = TableInstance<T> & {
  tableStyles?: TableStyleProps<T>;
};

export interface TableStyleProps<T extends object> {
  emptyText?: string;
  striped?: boolean;
  placeholder?: number;
  hideHeader?: boolean;
  fixHeader?: boolean;
  headersRenderer?: (headers: HeaderGroup<T>[]) => JSX.Element[];
  rowRenderer?: (row: Row<T>) => Nullable<JSX.Element>;
}

const useStyles = createStyles((theme) => {
  return {
    container: {
      display: "block",
      maxWidth: "100%",
      overflowX: "auto",
    },
    table: {
      borderCollapse: "collapse",
    },
    header: {},
  };
});

function DefaultHeaderRenderer<T extends object>(
  headers: HeaderGroup<T>[]
): JSX.Element[] {
  return headers.map((col) => (
    <th style={{ whiteSpace: "nowrap" }} {...col.getHeaderProps()}>
      {col.render("Header")}
    </th>
  ));
}

function DefaultRowRenderer<T extends object>(row: Row<T>): JSX.Element | null {
  return (
    <tr {...row.getRowProps()}>
      {row.cells.map((cell) => (
        <td {...cell.getCellProps()}>{cell.render("Cell")}</td>
      ))}
    </tr>
  );
}

export default function BaseTable<T extends object>(props: BaseTableProps<T>) {
  const {
    headerGroups,
    rows: tableRows,
    page: tablePages,
    prepareRow,
    getTableProps,
    getTableBodyProps,
    tableStyles,
  } = props;

  const headersRenderer = tableStyles?.headersRenderer ?? DefaultHeaderRenderer;
  const rowRenderer = tableStyles?.rowRenderer ?? DefaultRowRenderer;

  const { classes } = useStyles();

  const colCount = useMemo(() => {
    return headerGroups.reduce(
      (prev, curr) => (curr.headers.length > prev ? curr.headers.length : prev),
      0
    );
  }, [headerGroups]);

  // Switch to usePagination plugin if enabled
  const rows = tablePages ?? tableRows;

  const empty = rows.length === 0;

  const [pageSize] = usePageSize();
  const isLoading = useIsLoading();

  let body: ReactNode;
  if (isLoading) {
    body = Array(tableStyles?.placeholder ?? pageSize)
      .fill(0)
      .map((_, i) => (
        <tr key={i}>
          <td colSpan={colCount}>
            <Skeleton height={24}></Skeleton>
          </td>
        </tr>
      ));
  } else if (empty && tableStyles?.emptyText) {
    body = (
      <tr>
        <td colSpan={colCount}>
          <Text align="center">{tableStyles.emptyText}</Text>
        </td>
      </tr>
    );
  } else {
    body = rows.map((row) => {
      prepareRow(row);
      return rowRenderer(row);
    });
  }

  return (
    <Box className={classes.container}>
      <Table
        className={classes.table}
        striped={tableStyles?.striped ?? true}
        {...getTableProps()}
      >
        <thead className={classes.header} hidden={tableStyles?.hideHeader}>
          {headerGroups.map((headerGroup) => (
            <tr {...headerGroup.getHeaderGroupProps()}>
              {headersRenderer(headerGroup.headers)}
            </tr>
          ))}
        </thead>
        <tbody {...getTableBodyProps()}>{body}</tbody>
      </Table>
    </Box>
  );
}