From 366ca3bb3e2ed2c4034900a00c932f60dd90510d Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Sun, 5 Jan 2025 14:02:23 +0530 Subject: [PATCH] feat(generics): add a new performant resizable table (#6751) * feat(generics): add a new performant resizble table --- frontend/package.json | 2 + .../components/TableV3/TableV3.styles.scss | 55 +++++++ frontend/src/components/TableV3/TableV3.tsx | 135 ++++++++++++++++++ frontend/yarn.lock | 17 +++ 4 files changed, 209 insertions(+) create mode 100644 frontend/src/components/TableV3/TableV3.styles.scss create mode 100644 frontend/src/components/TableV3/TableV3.tsx diff --git a/frontend/package.json b/frontend/package.json index 6d6184a35d..b4a44e6592 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -43,6 +43,7 @@ "@sentry/react": "8.41.0", "@sentry/webpack-plugin": "2.22.6", "@signozhq/design-tokens": "1.1.4", + "@tanstack/react-table": "8.20.6", "@uiw/react-md-editor": "3.23.5", "@visx/group": "3.3.0", "@visx/shape": "3.5.0", @@ -153,6 +154,7 @@ "@babel/preset-typescript": "^7.21.4", "@commitlint/cli": "^16.3.0", "@commitlint/config-conventional": "^16.2.4", + "@faker-js/faker": "9.3.0", "@jest/globals": "^27.5.1", "@playwright/test": "^1.22.0", "@testing-library/jest-dom": "5.16.5", diff --git a/frontend/src/components/TableV3/TableV3.styles.scss b/frontend/src/components/TableV3/TableV3.styles.scss new file mode 100644 index 0000000000..b213698df7 --- /dev/null +++ b/frontend/src/components/TableV3/TableV3.styles.scss @@ -0,0 +1,55 @@ +.div-table { + border: 1px solid lightgray; + width: fit-content; +} + +.div-tr { + display: flex; + width: fit-content; + height: 30px; +} + +.div-th, +.div-td { + box-shadow: inset 0 0 0 1px lightgray; + padding: 0.25rem; +} + +.div-th { + padding: 2px 4px; + position: relative; + font-weight: bold; + text-align: center; + height: 30px; +} + +.div-td { + height: 30px; +} + +.resizer { + position: absolute; + top: 0; + height: 100%; + right: 0; + width: 5px; + background: rgba(0, 0, 0, 0.5); + cursor: col-resize; + user-select: none; + touch-action: none; +} + +.resizer.isResizing { + background: blue; + opacity: 1; +} + +@media (hover: hover) { + .resizer { + opacity: 0; + } + + *:hover > .resizer { + opacity: 1; + } +} diff --git a/frontend/src/components/TableV3/TableV3.tsx b/frontend/src/components/TableV3/TableV3.tsx new file mode 100644 index 0000000000..6219297f33 --- /dev/null +++ b/frontend/src/components/TableV3/TableV3.tsx @@ -0,0 +1,135 @@ +import './TableV3.styles.scss'; + +import { + ColumnDef, + flexRender, + getCoreRowModel, + Table, + useReactTable, +} from '@tanstack/react-table'; +import React, { useMemo } from 'react'; + +// here we are manually rendering the table body so that we can memoize the same for performant re-renders +function TableBody({ table }: { table: Table }): JSX.Element { + return ( +
+ {table.getRowModel().rows.map((row) => ( +
+ {row.getVisibleCells().map((cell) => ( +
+ {cell.renderValue()} +
+ ))} +
+ ))} +
+ ); +} + +// memoize the table body based on the data object being passed to the table +const MemoizedTableBody = React.memo( + TableBody, + (prev, next) => prev.table.options.data === next.table.options.data, +) as typeof TableBody; + +interface ITableConfig { + defaultColumnMinSize: number; + defaultColumnMaxSize: number; +} +interface ITableV3Props { + columns: ColumnDef[]; + data: T[]; + config: ITableConfig; +} + +export function TableV3(props: ITableV3Props): JSX.Element { + const { data, columns, config } = props; + + const table = useReactTable({ + data, + columns, + defaultColumn: { + minSize: config.defaultColumnMinSize, + maxSize: config.defaultColumnMaxSize, + }, + columnResizeMode: 'onChange', + getCoreRowModel: getCoreRowModel(), + debugTable: true, + debugHeaders: true, + debugColumns: true, + }); + + /** + * Instead of calling `column.getSize()` on every render for every header + * and especially every data cell (very expensive), + * we will calculate all column sizes at once at the root table level in a useMemo + * and pass the column sizes down as CSS variables to the element. + */ + const columnSizeVars = useMemo(() => { + const headers = table.getFlatHeaders(); + const colSizes: { [key: string]: number } = {}; + for (let i = 0; i < headers.length; i++) { + const header = headers[i]!; + colSizes[`--header-${header.id}-size`] = header.getSize(); + colSizes[`--col-${header.column.id}-size`] = header.column.getSize(); + } + return colSizes; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [table.getState().columnSizingInfo, table.getState().columnSizing]); + + return ( +
+ {/* Here in the
equivalent element (surrounds all table head and data cells), we will define our CSS variables for column sizes */} +
element + width: table.getTotalSize(), + }} + > +
+ {table.getHeaderGroups().map((headerGroup) => ( +
+ {headerGroup.headers.map((header) => ( +
+ {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} +
header.column.resetSize(), + onMouseDown: header.getResizeHandler(), + onTouchStart: header.getResizeHandler(), + className: `resizer ${ + header.column.getIsResizing() ? 'isResizing' : '' + }`, + }} + /> +
+ ))} +
+ ))} +
+ {/* When resizing any column we will render this special memoized version of our table body */} + {table.getState().columnSizingInfo.isResizingColumn ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 15e4b72c0f..460a0f89ab 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2599,6 +2599,11 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@faker-js/faker@9.3.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.3.0.tgz#ef398dab34c67faaa0e348318c905eae3564fa58" + integrity sha512-r0tJ3ZOkMd9xsu3VRfqlFR6cz0V/jFYRswAIpC+m/DIfAUXq7g8N7wTAlhSANySXYGKzGryfDXwtwsY8TxEIDw== + "@floating-ui/core@^1.4.2": version "1.5.2" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.2.tgz#53a0f7a98c550e63134d504f26804f6b83dbc071" @@ -3678,6 +3683,18 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@tanstack/react-table@8.20.6": + version "8.20.6" + resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.20.6.tgz#a1f3103327aa59aa621931f4087a7604a21054d0" + integrity sha512-w0jluT718MrOKthRcr2xsjqzx+oEM7B7s/XXyfs19ll++hlId3fjTm+B2zrR3ijpANpkzBAr15j1XGVOMxpggQ== + dependencies: + "@tanstack/table-core" "8.20.5" + +"@tanstack/table-core@8.20.5": + version "8.20.5" + resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d" + integrity sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg== + "@testing-library/dom@^8.5.0": version "8.20.0" resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz"