From 6dbecdfa615558eae1da5960f3e2eaf46ff215fd Mon Sep 17 00:00:00 2001 From: vikrantgupta25 Date: Sat, 4 Jan 2025 17:46:25 +0530 Subject: [PATCH 1/3] feat(generics): add a new performant resizble table --- frontend/package.json | 2 + .../PerformantColumnResizingTable.styles.scss | 73 ++++++++ .../PerformantColumnResizingTable.tsx | 164 ++++++++++++++++++ .../PerformantColumnResizingTable/makedata.ts | 31 ++++ frontend/src/pages/Services/index.tsx | 4 +- frontend/yarn.lock | 17 ++ 6 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.styles.scss create mode 100644 frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.tsx create mode 100644 frontend/src/components/PerformantColumnResizingTable/makedata.ts diff --git a/frontend/package.json b/frontend/package.json index 6d6184a35d..d7ba7142a6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,6 +34,7 @@ "@dnd-kit/core": "6.1.0", "@dnd-kit/modifiers": "7.0.0", "@dnd-kit/sortable": "8.0.0", + "@faker-js/faker": "9.3.0", "@grafana/data": "^11.2.3", "@mdx-js/loader": "2.3.0", "@mdx-js/react": "2.3.0", @@ -43,6 +44,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", diff --git a/frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.styles.scss b/frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.styles.scss new file mode 100644 index 0000000000..b2fc5d42aa --- /dev/null +++ b/frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.styles.scss @@ -0,0 +1,73 @@ +* { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + font-size: 14px; +} + +table, +.divTable { + border: 1px solid lightgray; + width: fit-content; +} + +.tr { + display: flex; +} + +tr, +.tr { + width: fit-content; + height: 30px; +} + +th, +.th, +td, +.td { + box-shadow: inset 0 0 0 1px lightgray; + padding: 0.25rem; +} + +th, +.th { + padding: 2px 4px; + position: relative; + font-weight: bold; + text-align: center; + height: 30px; +} + +td, +.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/PerformantColumnResizingTable/PerformantColumnResizingTable.tsx b/frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.tsx new file mode 100644 index 0000000000..94ecb307c6 --- /dev/null +++ b/frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.tsx @@ -0,0 +1,164 @@ +import './PerformantColumnResizingTable.styles.scss'; + +import { + ColumnDef, + createColumnHelper, + flexRender, + getCoreRowModel, + Table, + useReactTable, +} from '@tanstack/react-table'; +import { Button } from 'antd'; +import React, { useMemo, useState } from 'react'; + +import { makeData } from './makedata'; + +type Trace = { + spanName: string; + spanDuration: number; +}; + +function TableBody({ table }: { table: Table }): JSX.Element { + return ( +
+ {table.getRowModel().rows.map((row) => ( +
+ {row.getVisibleCells().map((cell) => ( +
+ {cell.renderValue()} +
+ ))} +
+ ))} +
+ ); +} + +const MemoizedTableBody = React.memo( + TableBody, + (prev, next) => prev.table.options.data === next.table.options.data, +) as typeof TableBody; + +const columnHelper = createColumnHelper(); +const defaultColumns: ColumnDef[] = [ + columnHelper.display({ + id: 'spanName', + cell: (props): JSX.Element => {props.row.original.spanName}, + }), + columnHelper.display({ + id: 'spanDuration', + enableResizing: false, + cell: (props): JSX.Element => {props.row.original.spanDuration}, + }), +]; + +export function PerformantColumnResizingTable(): JSX.Element { + const [data, setData] = useState(() => makeData(200)); + const [columns] = useState(() => [...defaultColumns]); + + const table = useReactTable({ + data, + columns, + defaultColumn: { + minSize: 60, + maxSize: 800, + }, + 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 ( +
+
+				{JSON.stringify(
+					{
+						columnSizing: table.getState().columnSizing,
+					},
+					null,
+					2,
+				)}
+			
+ +
({data.length} rows) +
+ {/* 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/src/components/PerformantColumnResizingTable/makedata.ts b/frontend/src/components/PerformantColumnResizingTable/makedata.ts new file mode 100644 index 0000000000..d3291950b4 --- /dev/null +++ b/frontend/src/components/PerformantColumnResizingTable/makedata.ts @@ -0,0 +1,31 @@ +import { faker } from '@faker-js/faker'; + +export type Trace = { + spanName: string; + spanDuration: number; +}; +const range = (len: number): number[] => { + const arr: number[] = []; + for (let i = 0; i < len; i++) { + arr.push(i); + } + return arr; +}; + +const newTrace = (): Trace => ({ + spanName: faker.person.firstName(), + spanDuration: faker.number.int(40), +}); + +export function makeData(...lens: number[]): Trace[] { + const makeDataLevel = (depth = 0): Trace[] => { + const len = lens[depth]!; + return range(len).map( + (): Trace => ({ + ...newTrace(), + }), + ); + }; + + return makeDataLevel(); +} diff --git a/frontend/src/pages/Services/index.tsx b/frontend/src/pages/Services/index.tsx index 2c2b1f5c17..2d6591718b 100644 --- a/frontend/src/pages/Services/index.tsx +++ b/frontend/src/pages/Services/index.tsx @@ -1,6 +1,6 @@ import { Space } from 'antd'; +import { PerformantColumnResizingTable } from 'components/PerformantColumnResizingTable/PerformantColumnResizingTable'; import ReleaseNote from 'components/ReleaseNote'; -import ServicesApplication from 'container/ServiceApplication'; import { useLocation } from 'react-router-dom'; function Metrics(): JSX.Element { @@ -10,7 +10,7 @@ function Metrics(): JSX.Element { - + ); } 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" From 00647eb398c17a97a411fdfcb79312379bce60de Mon Sep 17 00:00:00 2001 From: vikrantgupta25 Date: Sat, 4 Jan 2025 18:42:06 +0530 Subject: [PATCH 2/3] feat(generics): remove unwanted code and make the table use generics --- frontend/package.json | 2 +- .../PerformantColumnResizingTable.styles.scss | 9 -- .../PerformantColumnResizingTable.tsx | 149 +++++++----------- .../PerformantColumnResizingTable/makedata.ts | 31 ---- frontend/src/pages/Services/index.tsx | 4 +- 5 files changed, 64 insertions(+), 131 deletions(-) delete mode 100644 frontend/src/components/PerformantColumnResizingTable/makedata.ts diff --git a/frontend/package.json b/frontend/package.json index d7ba7142a6..b4a44e6592 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,7 +34,6 @@ "@dnd-kit/core": "6.1.0", "@dnd-kit/modifiers": "7.0.0", "@dnd-kit/sortable": "8.0.0", - "@faker-js/faker": "9.3.0", "@grafana/data": "^11.2.3", "@mdx-js/loader": "2.3.0", "@mdx-js/react": "2.3.0", @@ -155,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/PerformantColumnResizingTable/PerformantColumnResizingTable.styles.scss b/frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.styles.scss index b2fc5d42aa..1f4eed0001 100644 --- a/frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.styles.scss +++ b/frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.styles.scss @@ -1,12 +1,3 @@ -* { - box-sizing: border-box; -} - -html { - font-family: sans-serif; - font-size: 14px; -} - table, .divTable { border: 1px solid lightgray; diff --git a/frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.tsx b/frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.tsx index 94ecb307c6..eca11e0bd0 100644 --- a/frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.tsx +++ b/frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.tsx @@ -2,35 +2,24 @@ import './PerformantColumnResizingTable.styles.scss'; import { ColumnDef, - createColumnHelper, flexRender, getCoreRowModel, Table, useReactTable, } from '@tanstack/react-table'; -import { Button } from 'antd'; -import React, { useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; -import { makeData } from './makedata'; - -type Trace = { - spanName: string; - spanDuration: number; -}; - -function TableBody({ table }: { table: Table }): JSX.Element { +// 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) => (
}): JSX.Element { ); } +// 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; -const columnHelper = createColumnHelper(); -const defaultColumns: ColumnDef[] = [ - columnHelper.display({ - id: 'spanName', - cell: (props): JSX.Element => {props.row.original.spanName}, - }), - columnHelper.display({ - id: 'spanDuration', - enableResizing: false, - cell: (props): JSX.Element => {props.row.original.spanDuration}, - }), -]; +interface ITableConfig { + defaultColumnMinSize: number; + defaultColumnMaxSize: number; +} +interface IPerformantColumnResizingTableProps { + columns: ColumnDef[]; + data: T[]; + config: ITableConfig; +} -export function PerformantColumnResizingTable(): JSX.Element { - const [data, setData] = useState(() => makeData(200)); - const [columns] = useState(() => [...defaultColumns]); +export function PerformantColumnResizingTable( + props: IPerformantColumnResizingTableProps, +): JSX.Element { + const { data, columns, config } = props; const table = useReactTable({ data, columns, defaultColumn: { - minSize: 60, - maxSize: 800, + minSize: config.defaultColumnMinSize, + maxSize: config.defaultColumnMaxSize, }, columnResizeMode: 'onChange', getCoreRowModel: getCoreRowModel(), @@ -100,64 +88,49 @@ export function PerformantColumnResizingTable(): JSX.Element { return (
-
-				{JSON.stringify(
-					{
-						columnSizing: table.getState().columnSizing,
-					},
-					null,
-					2,
-				)}
-			
- -
({data.length} rows) -
- {/* 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) => ( + {/* 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' : '' + }`, }} - > - {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 ? ( - - ) : ( - - )} + /> +
+ ))} +
+ ))}
+ {/* When resizing any column we will render this special memoized version of our table body */} + {table.getState().columnSizingInfo.isResizingColumn ? ( + + ) : ( + + )} ); diff --git a/frontend/src/components/PerformantColumnResizingTable/makedata.ts b/frontend/src/components/PerformantColumnResizingTable/makedata.ts deleted file mode 100644 index d3291950b4..0000000000 --- a/frontend/src/components/PerformantColumnResizingTable/makedata.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { faker } from '@faker-js/faker'; - -export type Trace = { - spanName: string; - spanDuration: number; -}; -const range = (len: number): number[] => { - const arr: number[] = []; - for (let i = 0; i < len; i++) { - arr.push(i); - } - return arr; -}; - -const newTrace = (): Trace => ({ - spanName: faker.person.firstName(), - spanDuration: faker.number.int(40), -}); - -export function makeData(...lens: number[]): Trace[] { - const makeDataLevel = (depth = 0): Trace[] => { - const len = lens[depth]!; - return range(len).map( - (): Trace => ({ - ...newTrace(), - }), - ); - }; - - return makeDataLevel(); -} diff --git a/frontend/src/pages/Services/index.tsx b/frontend/src/pages/Services/index.tsx index 2d6591718b..2c2b1f5c17 100644 --- a/frontend/src/pages/Services/index.tsx +++ b/frontend/src/pages/Services/index.tsx @@ -1,6 +1,6 @@ import { Space } from 'antd'; -import { PerformantColumnResizingTable } from 'components/PerformantColumnResizingTable/PerformantColumnResizingTable'; import ReleaseNote from 'components/ReleaseNote'; +import ServicesApplication from 'container/ServiceApplication'; import { useLocation } from 'react-router-dom'; function Metrics(): JSX.Element { @@ -10,7 +10,7 @@ function Metrics(): JSX.Element { - + ); } From 42e03618024b5a867b188c6f4be359b7a7e48ebd Mon Sep 17 00:00:00 2001 From: vikrantgupta25 Date: Sun, 5 Jan 2025 13:43:00 +0530 Subject: [PATCH 3/3] feat(generics): better naming and remove global css selectors --- .../TableV3.styles.scss} | 21 +++++----------- .../TableV3.tsx} | 24 +++++++++---------- 2 files changed, 17 insertions(+), 28 deletions(-) rename frontend/src/components/{PerformantColumnResizingTable/PerformantColumnResizingTable.styles.scss => TableV3/TableV3.styles.scss} (88%) rename frontend/src/components/{PerformantColumnResizingTable/PerformantColumnResizingTable.tsx => TableV3/TableV3.tsx} (88%) diff --git a/frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.styles.scss b/frontend/src/components/TableV3/TableV3.styles.scss similarity index 88% rename from frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.styles.scss rename to frontend/src/components/TableV3/TableV3.styles.scss index 1f4eed0001..b213698df7 100644 --- a/frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.styles.scss +++ b/frontend/src/components/TableV3/TableV3.styles.scss @@ -1,29 +1,21 @@ -table, -.divTable { +.div-table { border: 1px solid lightgray; width: fit-content; } -.tr { +.div-tr { display: flex; -} - -tr, -.tr { width: fit-content; height: 30px; } -th, -.th, -td, -.td { +.div-th, +.div-td { box-shadow: inset 0 0 0 1px lightgray; padding: 0.25rem; } -th, -.th { +.div-th { padding: 2px 4px; position: relative; font-weight: bold; @@ -31,8 +23,7 @@ th, height: 30px; } -td, -.td { +.div-td { height: 30px; } diff --git a/frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.tsx b/frontend/src/components/TableV3/TableV3.tsx similarity index 88% rename from frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.tsx rename to frontend/src/components/TableV3/TableV3.tsx index eca11e0bd0..6219297f33 100644 --- a/frontend/src/components/PerformantColumnResizingTable/PerformantColumnResizingTable.tsx +++ b/frontend/src/components/TableV3/TableV3.tsx @@ -1,4 +1,4 @@ -import './PerformantColumnResizingTable.styles.scss'; +import './TableV3.styles.scss'; import { ColumnDef, @@ -12,13 +12,13 @@ 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) => (
{ - columns: ColumnDef[]; +interface ITableV3Props { + columns: ColumnDef[]; data: T[]; config: ITableConfig; } -export function PerformantColumnResizingTable( - props: IPerformantColumnResizingTableProps, -): JSX.Element { +export function TableV3(props: ITableV3Props): JSX.Element { const { data, columns, config } = props; const table = useReactTable({ @@ -90,19 +88,19 @@ export function PerformantColumnResizingTable(
{/* 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) => (