summaryrefslogtreecommitdiffhomepage
path: root/frontend/src/components/tables/BaseTable.tsx
blob: a3ba1a2f3849ef9ba8a5a84699be6b380497b1b5 (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
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,
    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]);

  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>
  );
}