From c63a0eb08c7ecedcc3fbee045dcc540ee0ae4b03 Mon Sep 17 00:00:00 2001 From: Chintan Mehta <22376522+chinmehta@users.noreply.github.com> Date: Wed, 15 Jan 2025 13:48:37 +0000 Subject: [PATCH] [Platform]: tanstack server side table with initial loading (#629) --------- Co-authored-by: Carlos Cruz --- .../components/Table/SectionRender.jsx | 19 +- .../ui/src/components/OtTable/OtTable.tsx | 23 +- .../ui/src/components/OtTable/OtTableSSP.tsx | 395 ++++++++++++++++++ .../OtTable/context/otTableActions.ts | 43 ++ .../OtTable/context/otTableReducer.ts | 62 +++ .../OtTable/service/tableService.ts | 23 + .../ui/src/components/OtTable/table.types.ts | 49 --- .../components/OtTable/types/tableTypes.ts | 108 +++++ .../{tableUtil.ts => utils/tableUtils.ts} | 5 +- packages/ui/src/index.tsx | 1 + 10 files changed, 655 insertions(+), 73 deletions(-) create mode 100644 packages/ui/src/components/OtTable/OtTableSSP.tsx create mode 100644 packages/ui/src/components/OtTable/context/otTableActions.ts create mode 100644 packages/ui/src/components/OtTable/context/otTableReducer.ts create mode 100644 packages/ui/src/components/OtTable/service/tableService.ts delete mode 100644 packages/ui/src/components/OtTable/table.types.ts create mode 100644 packages/ui/src/components/OtTable/types/tableTypes.ts rename packages/ui/src/components/OtTable/{tableUtil.ts => utils/tableUtils.ts} (98%) diff --git a/apps/platform/src/components/AssociationsToolkit/components/Table/SectionRender.jsx b/apps/platform/src/components/AssociationsToolkit/components/Table/SectionRender.jsx index 68ba67aa9..e3852aad8 100644 --- a/apps/platform/src/components/AssociationsToolkit/components/Table/SectionRender.jsx +++ b/apps/platform/src/components/AssociationsToolkit/components/Table/SectionRender.jsx @@ -1,6 +1,6 @@ import { Suspense } from "react"; import { styled } from "@mui/material/styles"; -import { LoadingBackdrop } from "ui"; +import { SectionLoader } from "ui"; import { ENTITIES } from "../../utils"; import prioritisationColumns from "../../static_datasets/prioritisationColumns"; @@ -8,15 +8,7 @@ import targetSections from "../../../../sections/targetSections"; import evidenceSections from "../../../../sections/evidenceSections"; import { grey } from "@mui/material/colors"; - -const LoadingContainer = styled("div")({ - margin: "25px 0", - height: "100px", - display: "flex", - justifyContent: "center", - alignItems: "center", - gap: "20px", -}); +import { Box } from "@mui/material"; const Container = styled("div", { shouldForwardProp: prop => prop !== "table", @@ -30,10 +22,9 @@ const Container = styled("div", { function LoadingSection() { return ( - - - Importing section assets - + + + ); } diff --git a/packages/ui/src/components/OtTable/OtTable.tsx b/packages/ui/src/components/OtTable/OtTable.tsx index c590406b4..88a5202d6 100644 --- a/packages/ui/src/components/OtTable/OtTable.tsx +++ b/packages/ui/src/components/OtTable/OtTable.tsx @@ -1,4 +1,4 @@ -import { ReactElement, ReactNode, useEffect, useMemo, useState } from "react"; +import { ReactElement, ReactNode, useMemo, useState } from "react"; import { Box, Grid, IconButton, NativeSelect, Skeleton } from "@mui/material"; import { useReactTable, @@ -26,7 +26,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import OtTableColumnFilter from "./OtTableColumnFilter"; // import { naLabel } from "../../constants"; import OtTableSearch from "./OtTableSearch"; -import { OtTableProps } from "./table.types"; +import { OtTableProps } from "./types/tableTypes"; import { FontAwesomeIconPadded, OtTableContainer, @@ -42,8 +42,9 @@ import { getDefaultSortObj, getFilterValueFromObject, getLoadingRows, + isNestedColumns, mapTableColumnToTanstackColumns, -} from "./tableUtil"; +} from "./utils/tableUtils"; import Tooltip from "../Tooltip"; import OtTableColumnVisibility from "./OtTableColumnVisibility"; @@ -51,6 +52,7 @@ declare module "@tanstack/table-core" { interface FilterFns { searchFilterFn: FilterFn; } + interface FilterMeta { itemRank: RankingInfo; } @@ -309,10 +311,17 @@ function OtTable({ } function getLoadingCells(columms: Array>) { - return columms.map(column => ({ - ...column, - cell: () => , - })); + const arr: Record[] = []; + columms.forEach(e => { + if (isNestedColumns(e)) { + const headerObj = { + header: e.header || e.label, + columns: getLoadingCells(e.columns), + }; + arr.push(headerObj); + } else arr.push({ ...e, cell: () => }); + }); + return arr; } export default OtTable; diff --git a/packages/ui/src/components/OtTable/OtTableSSP.tsx b/packages/ui/src/components/OtTable/OtTableSSP.tsx new file mode 100644 index 000000000..18e3a0d06 --- /dev/null +++ b/packages/ui/src/components/OtTable/OtTableSSP.tsx @@ -0,0 +1,395 @@ +import { ReactElement, ReactNode, useEffect, useMemo, useReducer, useState } from "react"; +import { Box, CircularProgress, Grid, IconButton, NativeSelect, Skeleton } from "@mui/material"; +import { + useReactTable, + getCoreRowModel, + getPaginationRowModel, + flexRender, + PaginationState, +} from "@tanstack/react-table"; +import { faAngleLeft, faAngleRight, faBackwardStep } from "@fortawesome/free-solid-svg-icons"; + +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import OtTableSearch from "./OtTableSearch"; +import { INIT_PAGE_SIZE, OtTableSSPProps } from "./types/tableTypes"; +import { + OtTableContainer, + OtTableHeader, + OtTH, + OtTableHeaderText, + OtTD, + OtTableCellContainer, +} from "./otTableLayout"; +import DataDownloader from "../DataDownloader"; +import { + getCurrentPagePosition, + isNestedColumns, + mapTableColumnToTanstackColumns, +} from "./utils/tableUtils"; +import Tooltip from "../Tooltip"; +import OtTableColumnVisibility from "./OtTableColumnVisibility"; + +import { getTableRows } from "./service/tableService"; +import { createInitialState, otTableReducer } from "./context/otTableReducer"; +import { addRows, setLoading, setNewData, textSearch } from "./context/otTableActions"; + +import useCursorBatchDownloader from "../../hooks/useCursorBatchDownloader"; + +function OtTableSSP({ + showGlobalFilter = true, + columns = [], + verticalHeaders = false, + query, + variables, + entity, + sectionName, + dataDownloaderFileStem, + dataDownloaderColumns, + dataDownloader, + showColumnVisibilityControl = true, + setInitialRequestData, +}: OtTableSSPProps): ReactElement { + const [state, dispatch] = useReducer(otTableReducer, "", createInitialState); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: INIT_PAGE_SIZE, + }); + const mappedColumns = mapTableColumnToTanstackColumns(columns); + const loadingCells = getLoadingCells(mappedColumns); + const tableColumns = useMemo( + () => (state.initialLoading ? loadingCells : mappedColumns), + [state.initialLoading] + ); + + const table = useReactTable({ + data: state.rows, + columns: tableColumns, + rowCount: state.count, + state: { + pagination, + }, + autoResetPageIndex: false, + // manualPagination: true, + onPaginationChange: onPaginationChange, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + }); + + /********************************************** + * DEFAULT FUNCTION CALLBACK TRIGGERED BY + * REACT TABLE IN ANY PAGE CHANGE EVENT * + * @param: + * updater: @type callback function + **********************************************/ + + function onPaginationChange(updater) { + const newPagination = updater(pagination); + + // switch () { + // case (pagination.pageSize !== newPagination.pageSize): { + // onPageSizeChange(newPagination); + // break; + // } + // case (newPagination.pageIndex > pagination.pageIndex): { + // onPageChange(newPagination); + // break; + // } + // default: { + // setPagination(newPagination); + // break; + // } + // } + + if (pagination.pageSize !== newPagination.pageSize) { + onPageSizeChange(newPagination); + } else if (newPagination.pageIndex > pagination.pageIndex) { + onPageChange(newPagination); + } else { + setPagination(newPagination); + } + } + + /********************************************** + * FUNCTION FOR PAGE INDEX CHANGE + * CHECK IF MORE DATA IS REQUIRED BY CHECKING + * NUMBER OF ROWS ALREADY FETCHED, PREVENT EXCESSIVE + * API CALLS IN CASE USER PAGINATE BACK AND FORTH + * @param: + * newPagination: @type PaginationState "@tanstack/react-table" + **********************************************/ + function onPageChange(newPagination: PaginationState) { + if (needMoreData(pagination.pageSize, newPagination.pageIndex)) { + addNewData(newPagination); + } else { + setPagination(newPagination); + } + } + + /********************************************** + * FUNCTION FOR PAGE SIZE CHANGE + * @param: + * newPagination: @type PaginationState "@tanstack/react-table" + **********************************************/ + function onPageSizeChange(newPagination: PaginationState) { + setTableData({ newPagination }); + } + + /********************************************** + * FUNCTION TO FETCH MORE DATA (PAGINATION) + * SETTING LOADING TRUE BEFORE FETCHING + * FETCH DATA AS PER TABLE STATE + * SETS PAGINATION AFTER DATA IS FETCHED TO AVOID + * SHOWING EMPTY ROWS + * @param: + * newPagination: @type PaginationState "@tanstack/react-table" + **********************************************/ + + function addNewData(newPagination: PaginationState) { + dispatch(setLoading(true)); + getTableRows({ + query, + variables, + cursor: state.cursor, + size: pagination.pageSize, + freeTextQuery: state.freeTextQuery, + }).then(d => { + dispatch(addRows(d.data[entity][sectionName])); + setPagination(newPagination); + }); + } + + /********************************************** + * FUNCTION TO FETCH NEW DATA (PAGE SIZE CHANGE OR SEARCH TEXT) + * SETTING LOADING TRUE BEFORE FETCHING + * FETCH DATA AND PASSING CURSOR AS NULL + * @param: + * newPagination: @type PaginationState "@tanstack/react-table" + **********************************************/ + function setTableData({ newPagination = pagination, freeTextQuery = state.freeTextQuery }) { + dispatch(setLoading(true)); + getTableRows({ + query, + variables, + cursor: null, + size: newPagination.pageSize, + freeTextQuery, + }).then(d => { + dispatch(setNewData(d.data[entity][sectionName])); + if (!state.freeTextQuery) setInitialRequestData(d); + }); + } + + /********************************************** + * FUNCTION TO TO CHECK IF MORE DATA IS NEEDED + * IN CASE USER PAGINATE BACKWARDS + * @param: + * pageSize: number + * pageIndex: number + * @return : boolean + **********************************************/ + function needMoreData(pageSize: number, pageIndex: number) { + const dataLength = table.options.data.length; + return dataLength < (pageIndex + 1) * pageSize; + } + + /********************************* + * STORES ALL DATA IN FOR EXPORT * + *********************************/ + const getWholeDataset = useCursorBatchDownloader( + query, + { ...variables, freeTextQuery: state.freeTextQuery }, + `data[${entity}][${sectionName}]` + ); + + useEffect(() => { + const newPagination = { + pageIndex: 0, + pageSize: pagination.pageSize, + }; + setTableData({ newPagination, freeTextQuery: state.freeTextQuery }); + }, [state.freeTextQuery]); + + function getCellData(cell: Record): ReactNode { + return <>{flexRender(cell.column.columnDef.cell, cell.getContext())}; + } + + return ( +
+ {/* Global Search */} + + + {showGlobalFilter && ( + { + dispatch(textSearch(freeTextQuery)); + }} + /> + )} + + + + {showColumnVisibilityControl && } + + {dataDownloader && ( + + )} + + + {/* Table component container */} + theme.spacing(3) }}> + {/* Table component */} + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => { + return ( + + {header.isPlaceholder ? null : ( + + + + {flexRender(header.column.columnDef.header, header.getContext())} + + + + )} + + ); + })} + + ))} + + + {table.getRowModel().rows.map(row => { + return ( + + {row.getVisibleCells().map(cell => { + return ( + + + {getCellData(cell)} + {/* {flexRender(cell.column.columnDef.cell, cell.getContext())} */} + {/* TODO: check NA value */} + {/* {Boolean(flexRender(cell.column.columnDef.cell, cell.getContext())) || + naLabel} */} + + + ); + })} + + ); + })} + + + + + {/* Table footer component container */} + `${theme.spacing(2)} 0 `, + }} + > + {state.loading && theme.spacing(2) }} size={25} />} +
+ Rows per page: + theme.spacing(2) }} + value={pagination.pageSize} + onChange={e => { + table.setPageSize(Number(e.target.value)); + setPagination({ + pageIndex: 0, + pageSize: Number(e.target.value), + }); + }} + > + {/* TODO: set page size */} + {[10, 25, 100].map(pageSize => ( + + ))} + +
+ theme.spacing(3), + marginLeft: theme => theme.spacing(3), + }} + > +
+ + {getCurrentPagePosition(pagination.pageIndex, pagination.pageSize, state.count)} + +
+ +
+ table.setPageIndex(0)} + disabled={!table.getCanPreviousPage() || state.loading} + > + + + table.previousPage()} + disabled={!table.getCanPreviousPage() || state.loading} + > + + + + table.nextPage()} + disabled={!table.getCanNextPage() || state.loading} + > + + +
+
+
+
+ ); +} + +// TODO: FIND A WAY TO USE SAME FUNCTION FROM CLIENT TABLE +function getLoadingCells(columms: Array>) { + const arr: Record[] = []; + columms.forEach(e => { + if (isNestedColumns(e)) { + const headerObj = { + header: e.header || e.label, + columns: getLoadingCells(e.columns), + }; + arr.push(headerObj); + } else arr.push({ ...e, cell: () => }); + }); + return arr; +} + +export default OtTableSSP; diff --git a/packages/ui/src/components/OtTable/context/otTableActions.ts b/packages/ui/src/components/OtTable/context/otTableActions.ts new file mode 100644 index 000000000..911691d1a --- /dev/null +++ b/packages/ui/src/components/OtTable/context/otTableActions.ts @@ -0,0 +1,43 @@ +import { Action, ActionType } from "../types/tableTypes"; + +export function onPageSizeChange(pageSize: number): Action { + return { + type: ActionType.PAGE_SIZE_CHANGE, + pageSize, + }; +} + +export function onPageChange(payload: Record): Action { + return { + type: ActionType.PAGE_CHANGE, + payload, + }; +} + +export function textSearch(freeQueryText: string): Action { + return { + type: ActionType.TEXT_SEARCH, + freeQueryText, + }; +} + +export function setLoading(loading: boolean): Action { + return { + type: ActionType.SET_LOADING, + loading, + }; +} + +export function addRows(payload: Record): Action { + return { + type: ActionType.ADD_DATA, + payload, + }; +} + +export function setNewData(payload: Record): Action { + return { + type: ActionType.SET_DATA, + payload, + }; +} diff --git a/packages/ui/src/components/OtTable/context/otTableReducer.ts b/packages/ui/src/components/OtTable/context/otTableReducer.ts new file mode 100644 index 000000000..6c69683b9 --- /dev/null +++ b/packages/ui/src/components/OtTable/context/otTableReducer.ts @@ -0,0 +1,62 @@ +import { Action, ActionType, OtTableSSPState } from "../types/tableTypes"; +import { getLoadingRows } from "../utils/tableUtils"; + +export const initialState: OtTableSSPState = { + count: 0, + loading: true, + rows: getLoadingRows(10), + cursor: null, + freeTextQuery: "", + initialLoading: true, +}; + +export function createInitialState(str: string): OtTableSSPState { + return initialState; +} + +export function otTableReducer( + state: OtTableSSPState = initialState, + action: Action +): OtTableSSPState { + if (typeof state === undefined) { + throw Error("State provided to table reducer is undefined"); + } + switch (action.type) { + case ActionType.SET_LOADING: { + return { + ...state, + loading: action.loading, + }; + } + case ActionType.TEXT_SEARCH: { + return { + ...state, + loading: true, + freeTextQuery: action.freeQueryText, + cursor: null, + }; + } + case ActionType.SET_DATA: { + return { + ...state, + initialLoading: false, + loading: false, + count: action.payload.count, + cursor: action.payload.cursor, + rows: action.payload.rows, + }; + } + case ActionType.ADD_DATA: { + return { + ...state, + loading: false, + cursor: action.payload.cursor, + rows: [...state.rows, ...action.payload.rows], + }; + } + default: { + throw Error("Unknown action: " + action); + return state; + } + } +} diff --git a/packages/ui/src/components/OtTable/service/tableService.ts b/packages/ui/src/components/OtTable/service/tableService.ts new file mode 100644 index 000000000..6fd78c20e --- /dev/null +++ b/packages/ui/src/components/OtTable/service/tableService.ts @@ -0,0 +1,23 @@ +import client from "../../../client"; +import { getTableRowsProps } from "../types/tableTypes"; + +export async function getTableRows({ + query, + variables, + cursor, + size, + freeTextQuery, +}: getTableRowsProps): Promise<[]> { + const resData = await client.query({ + query, + variables: { + ...variables, + cursor, + size, + freeTextQuery, + }, + fetchPolicy: "no-cache", + }); + // check and return only rows/count/cursor + return resData; +} diff --git a/packages/ui/src/components/OtTable/table.types.ts b/packages/ui/src/components/OtTable/table.types.ts deleted file mode 100644 index 26534e28b..000000000 --- a/packages/ui/src/components/OtTable/table.types.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { DocumentNode } from "@apollo/client"; -import { ColumnDef, Table } from "@tanstack/table-core"; - -/****************************** - * OT TABLE CLIENT SIDE TYPES * - ******************************/ - -export type DefaultSortProp = - | [ - { - id: string; - desc: boolean; - } - ] - | undefined; - -export type OtTableProps = { - showGlobalFilter: boolean; - tableDataLoading: boolean; - columns: Array>; - rows: Array>; - verticalHeaders: boolean; - order: "asc" | "desc"; - sortBy: string; - defaultSortObj: DefaultSortProp; - dataDownloader: boolean; - dataDownloaderColumns?: Array>; - dataDownloaderFileStem: string; - query: DocumentNode; - variables: Record; - showColumnVisibilityControl: boolean; - loading: boolean; -}; - -export type loadingTableRows = { - id: Record; -}; - -/************************* - * OT TABLE SEARCH TYPES * - *************************/ - -export type OtTableSearchProps = { - setGlobalSearchTerm: React.Dispatch>; -}; - -export type OtTableColumnVisibilityProps = { - table: Table; -}; diff --git a/packages/ui/src/components/OtTable/types/tableTypes.ts b/packages/ui/src/components/OtTable/types/tableTypes.ts new file mode 100644 index 000000000..6f0c9ee0f --- /dev/null +++ b/packages/ui/src/components/OtTable/types/tableTypes.ts @@ -0,0 +1,108 @@ +import { DocumentNode } from "@apollo/client"; +import { ColumnDef, Table } from "@tanstack/table-core"; + +export const INIT_PAGE_SIZE = 10; + +/****************************** + * OT TABLE CLIENT SIDE TYPES * + ******************************/ + +export type DefaultSortProp = + | [ + { + id: string; + desc: boolean; + } + ] + | undefined; + +export type OtTableProps = { + showGlobalFilter: boolean; + tableDataLoading: boolean; + columns: Array>; + rows: Array>; + verticalHeaders: boolean; + order: "asc" | "desc"; + sortBy: string; + defaultSortObj: DefaultSortProp; + dataDownloader: boolean; + dataDownloaderColumns?: Array>; + dataDownloaderFileStem: string; + query: DocumentNode; + variables: Record; + showColumnVisibilityControl: boolean; + loading: boolean; +}; + +export type loadingTableRows = { + id: Record; +}; + +/************************* + * OT TABLE SEARCH TYPES * + *************************/ + +export type OtTableSearchProps = { + setGlobalSearchTerm: React.Dispatch>; +}; + +export type OtTableColumnVisibilityProps = { + table: Table; +}; + +/****************************** + * OT TABLE SERVER SIDE TYPES * + ******************************/ + +export type OtTableSSPProps = { + showGlobalFilter: boolean; + entity: string; + sectionName: string; + verticalHeaders: boolean; + columns: Array>; + query: DocumentNode; + variables: Record; + dataDownloaderFileStem: string; + dataDownloader: boolean; + dataDownloaderColumns?: Array>; + showColumnVisibilityControl: boolean; + setInitialRequestData: any; +}; + +export type OtTableSSPState = { + count: number; + loading: boolean; + rows: Array; + cursor: null | string; + freeTextQuery: null | string; + initialLoading: boolean; +}; + +export type getTableRowsProps = { + size: number; + query: DocumentNode; + cursor: string | null; + freeTextQuery: string | null; + variables: Record; +}; + +/***************** + * ACTIONS TYPES * + *****************/ + +export enum ActionType { + PAGE_SIZE_CHANGE = "PAGE_SIZE_CHANGE", + PAGE_CHANGE = "PAGE_CHANGE", + TEXT_SEARCH = "TEXT_SEARCH", + SET_LOADING = "SET_LOADING", + SET_DATA = "SET_DATA", + ADD_DATA = "ADD_DATA", +} + +export type Action = + | { type: ActionType.PAGE_SIZE_CHANGE; pageSize: number } + | { type: ActionType.PAGE_CHANGE; payload: Record } + | { type: ActionType.TEXT_SEARCH; freeQueryText: string } + | { type: ActionType.SET_DATA; payload: Record } + | { type: ActionType.ADD_DATA; payload: Record } + | { type: ActionType.SET_LOADING; loading: boolean }; diff --git a/packages/ui/src/components/OtTable/tableUtil.ts b/packages/ui/src/components/OtTable/utils/tableUtils.ts similarity index 98% rename from packages/ui/src/components/OtTable/tableUtil.ts rename to packages/ui/src/components/OtTable/utils/tableUtils.ts index ce3491c00..4b30c6aaa 100644 --- a/packages/ui/src/components/OtTable/tableUtil.ts +++ b/packages/ui/src/components/OtTable/utils/tableUtils.ts @@ -11,7 +11,7 @@ export function mapTableColumnToTanstackColumns( allColumns.forEach(e => { if (isNestedColumns(e)) { const headerObj = { - header: e.header, + header: e.header || e.label, columns: mapTableColumnToTanstackColumns(e.columns), }; arr.push(headerObj); @@ -106,7 +106,6 @@ export function getCurrentPagePosition( *****************************************************************/ export function getLoadingRows(size = 10): loadingTableRows[] { const rows = new Array(size).fill({}); - console.log(rows); return rows; } @@ -116,7 +115,7 @@ export function getLoadingRows(size = 10): loadingTableRows[] { * column: object * @return: boolean ***********************************/ -function isNestedColumns(column: Record): boolean { +export function isNestedColumns(column: Record): boolean { return Object.hasOwn(column, "columns"); } diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx index 62b9502cc..534e7e146 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.tsx @@ -42,6 +42,7 @@ export { default as ApiPlaygroundDrawer } from "./components/ApiPlaygroundDrawer export { default as OtTable } from "./components/OtTable/OtTable"; export { default as OtPopper } from "./components/OtPopper"; export { default as OtScoreLinearBar } from "./components/OtScoreLinearBar"; +export { default as OtTableSSP } from "./components/OtTable/OtTableSSP"; export { default as EntityPanel } from "./components/EntityPanel/EntityPanel"; export { default as EmptyPage } from "./pages/EmptyPage";