Table.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. import React, { useEffect } from "react";
  2. import styled from "styled-components";
  3. import {
  4. Column,
  5. Row,
  6. useGlobalFilter,
  7. usePagination,
  8. useTable,
  9. } from "react-table";
  10. import Loading from "components/Loading";
  11. import Selector from "./Selector";
  12. import loading from "assets/loading.gif";
  13. const GlobalFilter: React.FunctionComponent<any> = ({
  14. setGlobalFilter,
  15. onRefresh,
  16. isRefreshing,
  17. }) => {
  18. const [value, setValue] = React.useState("");
  19. const onChange = (value: string) => {
  20. setValue(value);
  21. setGlobalFilter(value || undefined);
  22. };
  23. return (
  24. <SearchRowWrapper>
  25. <SearchRow>
  26. <i className="material-icons">search</i>
  27. <SearchInput
  28. value={value}
  29. onChange={(e: any) => {
  30. onChange(e.target.value);
  31. }}
  32. placeholder="Search"
  33. />
  34. </SearchRow>
  35. {typeof onRefresh === "function" && (
  36. <RefreshButton onClick={onRefresh} disabled={isRefreshing}>
  37. {isRefreshing ? (
  38. <>
  39. <img src={loading} alt="loading icon" />
  40. </>
  41. ) : (
  42. <i className="material-icons">refresh</i>
  43. )}
  44. </RefreshButton>
  45. )}
  46. </SearchRowWrapper>
  47. );
  48. };
  49. export type TableProps = {
  50. columns: Column<any>[];
  51. data: any[];
  52. onRowClick?: (row: Row) => void;
  53. isLoading: boolean;
  54. disableGlobalFilter?: boolean;
  55. disableHover?: boolean;
  56. enablePagination?: boolean;
  57. hasError?: boolean;
  58. errorMessage?: string;
  59. onRefresh?: () => void;
  60. isRefreshing?: boolean;
  61. };
  62. const MIN_PAGE_SIZE = 1;
  63. const Table: React.FC<TableProps> = ({
  64. columns: columnsData,
  65. data,
  66. onRowClick,
  67. isLoading,
  68. disableGlobalFilter = false,
  69. disableHover,
  70. enablePagination,
  71. hasError,
  72. errorMessage = "An unexpected error occurred, please try again.",
  73. onRefresh,
  74. isRefreshing = false,
  75. }) => {
  76. const {
  77. getTableProps,
  78. getTableBodyProps,
  79. page,
  80. setGlobalFilter,
  81. prepareRow,
  82. headerGroups,
  83. visibleColumns,
  84. // Pagination options
  85. canPreviousPage,
  86. canNextPage,
  87. pageOptions,
  88. pageCount,
  89. gotoPage,
  90. nextPage,
  91. previousPage,
  92. setPageSize,
  93. state: { pageIndex, pageSize },
  94. } = useTable(
  95. {
  96. columns: columnsData,
  97. data,
  98. },
  99. useGlobalFilter,
  100. usePagination
  101. );
  102. useEffect(() => {
  103. if (!enablePagination) {
  104. setPageSize(data.length || MIN_PAGE_SIZE);
  105. }
  106. }, [data, enablePagination]);
  107. const renderRows = () => {
  108. if (hasError) {
  109. return (
  110. <StyledTr disableHover={true} selected={false}>
  111. <StyledTd colSpan={visibleColumns.length} align="center">
  112. {errorMessage}
  113. </StyledTd>
  114. </StyledTr>
  115. );
  116. }
  117. if (isLoading) {
  118. return (
  119. <StyledTr disableHover={true} selected={false}>
  120. <StyledTd colSpan={visibleColumns.length} height="150px">
  121. <Loading />
  122. </StyledTd>
  123. </StyledTr>
  124. );
  125. }
  126. if (!page.length) {
  127. return (
  128. <StyledTr disableHover={true} selected={false}>
  129. <StyledTd colSpan={visibleColumns.length} align="center">
  130. No data available
  131. </StyledTd>
  132. </StyledTr>
  133. );
  134. }
  135. return (
  136. <>
  137. {page.map((row) => {
  138. prepareRow(row);
  139. return (
  140. <StyledTr
  141. disableHover={disableHover}
  142. {...row.getRowProps()}
  143. enablePointer={!!onRowClick}
  144. onClick={() => onRowClick && onRowClick(row)}
  145. selected={false}
  146. >
  147. {/* TODO: This is actually broken, not sure why but we need the width to be properly setted, this is a temporary solution */}
  148. {row.cells.map((cell) => {
  149. return (
  150. <StyledTd
  151. {...cell.getCellProps()}
  152. style={{
  153. width: cell.column.totalWidth,
  154. }}
  155. >
  156. {cell.render("Cell")}
  157. </StyledTd>
  158. );
  159. })}
  160. </StyledTr>
  161. );
  162. })}
  163. </>
  164. );
  165. };
  166. return (
  167. <TableWrapper>
  168. {!disableGlobalFilter && (
  169. <GlobalFilter
  170. setGlobalFilter={setGlobalFilter}
  171. onRefresh={onRefresh}
  172. isRefreshing={isRefreshing}
  173. />
  174. )}
  175. <StyledTable {...getTableProps()}>
  176. <StyledTHead>
  177. {headerGroups.map((headerGroup) => (
  178. <StyledTr
  179. {...headerGroup.getHeaderGroupProps()}
  180. disableHover={true}
  181. >
  182. {headerGroup.headers.map((column) => (
  183. <StyledTh {...column.getHeaderProps()}>
  184. {column.render("Header")}
  185. </StyledTh>
  186. ))}
  187. </StyledTr>
  188. ))}
  189. </StyledTHead>
  190. <tbody {...getTableBodyProps()}>{renderRows()}</tbody>
  191. </StyledTable>
  192. {enablePagination && (
  193. <FlexEnd style={{ marginTop: "15px" }}>
  194. <PageCountWrapper>
  195. Page size:
  196. <Selector
  197. activeValue={String(pageSize)}
  198. options={[
  199. {
  200. label: "10",
  201. value: "10",
  202. },
  203. {
  204. label: "20",
  205. value: "20",
  206. },
  207. {
  208. label: "50",
  209. value: "50",
  210. },
  211. {
  212. label: "100",
  213. value: "100",
  214. },
  215. ]}
  216. setActiveValue={(val) => setPageSize(Number(val))}
  217. width="70px"
  218. ></Selector>
  219. </PageCountWrapper>
  220. <PaginationActionsWrapper>
  221. <PaginationAction
  222. disabled={!canPreviousPage}
  223. onClick={previousPage}
  224. >
  225. {"<"}
  226. </PaginationAction>
  227. <PageCounter>
  228. {pageIndex + 1} of {pageCount}
  229. </PageCounter>
  230. <PaginationAction disabled={!canNextPage} onClick={nextPage}>
  231. {">"}
  232. </PaginationAction>
  233. </PaginationActionsWrapper>
  234. </FlexEnd>
  235. )}
  236. </TableWrapper>
  237. );
  238. };
  239. export default Table;
  240. const TableWrapper = styled.div`
  241. padding-bottom: 20px;
  242. `;
  243. const FlexEnd = styled.div`
  244. display: flex;
  245. justify-content: flex-end;
  246. align-items: center;
  247. width: 100%;
  248. `;
  249. const PaginationActionsWrapper = styled.div``;
  250. const PageCountWrapper = styled.div`
  251. display: flex;
  252. align-items: center;
  253. justify-content: space-between;
  254. min-width: 160px;
  255. margin-right: 10px;
  256. `;
  257. const PaginationAction = styled.button`
  258. border: none;
  259. background: unset;
  260. color: white;
  261. padding: 10px;
  262. cursor: pointer;
  263. border-radius: 5px;
  264. :hover {
  265. background: #ffffff40;
  266. }
  267. :disabled {
  268. color: #ffffff88;
  269. cursor: unset;
  270. :hover {
  271. background: unset;
  272. }
  273. }
  274. `;
  275. const PageCounter = styled.span`
  276. margin: 0 5px;
  277. `;
  278. type StyledTrProps = {
  279. enablePointer?: boolean;
  280. disableHover?: boolean;
  281. selected?: boolean;
  282. };
  283. export const StyledTr = styled.tr`
  284. line-height: 2.2em;
  285. background: ${(props: StyledTrProps) => (props.selected ? "#ffffff11" : "")};
  286. :hover {
  287. background: ${(props: StyledTrProps) =>
  288. props.disableHover ? "" : "#ffffff22"};
  289. }
  290. cursor: ${(props: StyledTrProps) =>
  291. props.enablePointer ? "pointer" : "unset"};
  292. `;
  293. export const StyledTd = styled.td`
  294. font-size: 13px;
  295. color: #ffffff;
  296. :first-child {
  297. padding-left: 10px;
  298. }
  299. :last-child {
  300. padding-right: 10px;
  301. }
  302. user-select: text;
  303. ${(props: { align?: "center" | "left" }) => {
  304. if (props.align) {
  305. return `text-align:${props.align};`;
  306. }
  307. }}
  308. `;
  309. export const StyledTHead = styled.thead`
  310. width: 100%;
  311. border-top: 1px solid #aaaabb22;
  312. border-bottom: 1px solid #aaaabb22;
  313. position: sticky;
  314. `;
  315. export const StyledTh = styled.th`
  316. text-align: left;
  317. font-size: 13px;
  318. font-weight: 500;
  319. color: #aaaabb;
  320. :first-child {
  321. padding-left: 10px;
  322. }
  323. :last-child {
  324. padding-right: 10px;
  325. }
  326. `;
  327. export const StyledTable = styled.table`
  328. width: 100%;
  329. min-width: 500px;
  330. border-collapse: collapse;
  331. `;
  332. const SearchInput = styled.input`
  333. outline: none;
  334. border: none;
  335. font-size: 13px;
  336. background: none;
  337. width: 100%;
  338. color: white;
  339. padding: 0;
  340. height: 20px;
  341. `;
  342. const SearchRow = styled.div`
  343. display: flex;
  344. width: 100%;
  345. font-size: 13px;
  346. color: #ffffff55;
  347. border-radius: 4px;
  348. user-select: none;
  349. align-items: center;
  350. padding: 10px 0px;
  351. min-width: 300px;
  352. max-width: min-content;
  353. background: #ffffff11;
  354. i {
  355. width: 18px;
  356. height: 18px;
  357. margin-left: 12px;
  358. margin-right: 12px;
  359. font-size: 20px;
  360. }
  361. `;
  362. const SearchRowWrapper = styled.div`
  363. display: flex;
  364. justify-content: space-between;
  365. align-items: center;
  366. margin-bottom: 15px;
  367. margin-top: 0px;
  368. `;
  369. const RefreshButton = styled.button`
  370. justify-self: flex-end;
  371. border: 1px solid #ffffff00;
  372. border-radius: 50%;
  373. background: inherit;
  374. color: #ffffff;
  375. cursor: pointer;
  376. display: flex;
  377. align-items: center;
  378. justify-content: center;
  379. width: 35px;
  380. height: 35px;
  381. > i {
  382. font-size: 20px;
  383. }
  384. > img {
  385. width: 20px;
  386. height: 20px;
  387. }
  388. :hover {
  389. color: #ffffff88;
  390. border-color: #ffffff88;
  391. }
  392. `;