From 1c88b4e9d2c379e0fc5ddeb42f1fbb8de8ab5e4e Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Tue, 22 Oct 2024 16:00:37 +0300 Subject: [PATCH 01/21] fix: very bad performance when scrolling paginated tables --- .../PaginatedTable/PaginatedTable.tsx | 5 +-- src/components/PaginatedTable/TableChunk.tsx | 7 ++-- .../PaginatedTable/useScrollBasedChunks.ts | 33 +++++++++++-------- .../PaginatedTable/{utils.ts => utils.tsx} | 4 +++ 4 files changed, 30 insertions(+), 19 deletions(-) rename src/components/PaginatedTable/{utils.ts => utils.tsx} (90%) diff --git a/src/components/PaginatedTable/PaginatedTable.tsx b/src/components/PaginatedTable/PaginatedTable.tsx index f874d4a55..2ce40de7c 100644 --- a/src/components/PaginatedTable/PaginatedTable.tsx +++ b/src/components/PaginatedTable/PaginatedTable.tsx @@ -69,9 +69,10 @@ export const PaginatedTable = ({ const tableRef = React.useRef(null); const activeChunks = useScrollBasedChunks({ - containerRef: parentRef ?? tableRef, + parentRef, + tableRef, totalItems: foundEntities, - itemHeight: rowHeight, + rowHeight, chunkSize: limit, }); diff --git a/src/components/PaginatedTable/TableChunk.tsx b/src/components/PaginatedTable/TableChunk.tsx index 75fc2a5c2..f48c47380 100644 --- a/src/components/PaginatedTable/TableChunk.tsx +++ b/src/components/PaginatedTable/TableChunk.tsx @@ -8,6 +8,7 @@ import {ResponseError} from '../Errors/ResponseError'; import {EmptyTableRow, LoadingTableRow, TableRow} from './TableRow'; import type {Column, FetchData, GetRowClassName, SortParams} from './types'; +import {typedMemo} from './utils'; const DEBOUNCE_TIMEOUT = 200; @@ -29,7 +30,7 @@ interface TableChunkProps { } // Memoisation prevents chunks rerenders that could cause perfomance issues on big tables -export const TableChunk = ({ +export const TableChunk = typedMemo(function TableChunk({ id, limit, totalLength, @@ -43,7 +44,7 @@ export const TableChunk = ({ renderErrorMessage, onDataFetched, isActive, -}: TableChunkProps) => { +}: TableChunkProps) { const [isTimeoutActive, setIsTimeoutActive] = React.useState(true); const [autoRefreshInterval] = useAutoRefreshInterval(); @@ -150,4 +151,4 @@ export const TableChunk = ({ {renderContent()} ); -}; +}); diff --git a/src/components/PaginatedTable/useScrollBasedChunks.ts b/src/components/PaginatedTable/useScrollBasedChunks.ts index 4b96812e3..ca0eca144 100644 --- a/src/components/PaginatedTable/useScrollBasedChunks.ts +++ b/src/components/PaginatedTable/useScrollBasedChunks.ts @@ -1,13 +1,14 @@ import React from 'react'; -import {throttle} from 'lodash'; +import {isEqual, throttle} from 'lodash'; import {getArray} from '../../utils'; interface UseScrollBasedChunksProps { - containerRef: React.RefObject; + parentRef?: React.RefObject; + tableRef: React.RefObject; totalItems: number; - itemHeight: number; + rowHeight: number; chunkSize: number; } @@ -15,9 +16,10 @@ const THROTTLE_DELAY = 100; const CHUNKS_AHEAD_COUNT = 1; export const useScrollBasedChunks = ({ - containerRef, + parentRef, + tableRef, totalItems, - itemHeight, + rowHeight, chunkSize, }: UseScrollBasedChunksProps): number[] => { const [activeChunks, setActiveChunks] = React.useState( @@ -25,15 +27,16 @@ export const useScrollBasedChunks = ({ ); const calculateActiveChunks = React.useCallback(() => { - const container = containerRef.current; - if (!container) { + const container = parentRef?.current; + const table = tableRef.current; + if (!container || !table) { return; } - const {scrollTop, clientHeight} = container; - const visibleStartIndex = Math.floor(scrollTop / itemHeight); + const tableScrollTop = Math.max(container.scrollTop - table.offsetTop, 0); + const visibleStartIndex = Math.floor(Math.max(tableScrollTop, 0) / rowHeight); const visibleEndIndex = Math.min( - Math.ceil((scrollTop + clientHeight) / itemHeight), + Math.ceil((tableScrollTop + container.clientHeight) / rowHeight), totalItems - 1, ); @@ -44,8 +47,10 @@ export const useScrollBasedChunks = ({ (index) => startChunk + index, ); - setActiveChunks(newActiveChunks); - }, [chunkSize, containerRef, itemHeight, totalItems]); + if (!isEqual(activeChunks, newActiveChunks)) { + setActiveChunks(newActiveChunks); + } + }, [parentRef, tableRef, rowHeight, totalItems, chunkSize, activeChunks]); const throttledCalculateActiveChunks = React.useMemo( () => throttle(calculateActiveChunks, THROTTLE_DELAY), @@ -53,7 +58,7 @@ export const useScrollBasedChunks = ({ ); React.useEffect(() => { - const container = containerRef.current; + const container = parentRef?.current; if (!container) { return undefined; } @@ -63,7 +68,7 @@ export const useScrollBasedChunks = ({ container.removeEventListener('scroll', throttledCalculateActiveChunks); throttledCalculateActiveChunks.cancel(); }; - }, [containerRef, throttledCalculateActiveChunks]); + }, [parentRef, throttledCalculateActiveChunks]); return activeChunks; }; diff --git a/src/components/PaginatedTable/utils.ts b/src/components/PaginatedTable/utils.tsx similarity index 90% rename from src/components/PaginatedTable/utils.ts rename to src/components/PaginatedTable/utils.tsx index e546d8e3e..d9fb6bb3b 100644 --- a/src/components/PaginatedTable/utils.ts +++ b/src/components/PaginatedTable/utils.tsx @@ -1,3 +1,5 @@ +import React from 'react'; + // invoke passed function at most once per animation frame // eslint-disable-next-line @typescript-eslint/no-explicit-any export function rafThrottle any>(fn: Fn) { @@ -23,3 +25,5 @@ export function rafThrottle any>(fn: Fn) { export function calculateColumnWidth(newWidth: number, minWidth = 40, maxWidth = Infinity) { return Math.max(minWidth, Math.min(newWidth, maxWidth)); } + +export const typedMemo: (Component: T) => T = React.memo; From 37e02c45b24eaf80beba4663f1d5a9db60f8e898 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Tue, 22 Oct 2024 23:01:58 +0300 Subject: [PATCH 02/21] fix: some performance optimizations --- .../PaginatedTable/PaginatedTable.tsx | 21 +++++++++++++----- .../PaginatedTable/useScrollBasedChunks.ts | 22 +++++++++---------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/components/PaginatedTable/PaginatedTable.tsx b/src/components/PaginatedTable/PaginatedTable.tsx index 2ce40de7c..057723f8f 100644 --- a/src/components/PaginatedTable/PaginatedTable.tsx +++ b/src/components/PaginatedTable/PaginatedTable.tsx @@ -76,11 +76,20 @@ export const PaginatedTable = ({ chunkSize: limit, }); - const handleDataFetched = React.useCallback((total: number, found: number) => { - setTotalEntities(total); - setFoundEntities(found); - setIsInitialLoad(false); - }, []); + const handleDataFetched = React.useCallback( + (total: number, found: number) => { + if (total !== totalEntities) { + setTotalEntities(total); + } + + if (found !== foundEntities) { + setFoundEntities(found); + } + + setIsInitialLoad(false); + }, + [foundEntities, totalEntities], + ); // reset table on filters change React.useLayoutEffect(() => { @@ -123,7 +132,7 @@ export const PaginatedTable = ({ getRowClassName={getRowClassName} renderErrorMessage={renderErrorMessage} onDataFetched={handleDataFetched} - isActive={activeChunks.includes(value)} + isActive={activeChunks[value]} /> )); }; diff --git a/src/components/PaginatedTable/useScrollBasedChunks.ts b/src/components/PaginatedTable/useScrollBasedChunks.ts index ca0eca144..c2fbd143b 100644 --- a/src/components/PaginatedTable/useScrollBasedChunks.ts +++ b/src/components/PaginatedTable/useScrollBasedChunks.ts @@ -2,8 +2,6 @@ import React from 'react'; import {isEqual, throttle} from 'lodash'; -import {getArray} from '../../utils'; - interface UseScrollBasedChunksProps { parentRef?: React.RefObject; tableRef: React.RefObject; @@ -21,10 +19,8 @@ export const useScrollBasedChunks = ({ totalItems, rowHeight, chunkSize, -}: UseScrollBasedChunksProps): number[] => { - const [activeChunks, setActiveChunks] = React.useState( - getArray(1 + CHUNKS_AHEAD_COUNT).map((index) => index), - ); +}: UseScrollBasedChunksProps): boolean[] => { + const [activeChunks, setActiveChunks] = React.useState([true, true]); const calculateActiveChunks = React.useCallback(() => { const container = parentRef?.current; @@ -40,12 +36,16 @@ export const useScrollBasedChunks = ({ totalItems - 1, ); - const startChunk = Math.floor(visibleStartIndex / chunkSize); - const endChunk = Math.floor(visibleEndIndex / chunkSize); - - const newActiveChunks = getArray(endChunk - startChunk + 1 + CHUNKS_AHEAD_COUNT).map( - (index) => startChunk + index, + const startChunk = Math.max( + Math.floor(visibleStartIndex / chunkSize) - CHUNKS_AHEAD_COUNT, + 0, ); + const endChunk = Math.floor(visibleEndIndex / chunkSize) + CHUNKS_AHEAD_COUNT; + + const newActiveChunks = []; + for (let i = startChunk; i <= endChunk; i++) { + newActiveChunks[i] = true; + } if (!isEqual(activeChunks, newActiveChunks)) { setActiveChunks(newActiveChunks); From 05a327133e51d9ac006d7369ab6e42c127f2cf10 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Wed, 23 Oct 2024 19:47:16 +0300 Subject: [PATCH 03/21] fix: parentRef problems and microoptimizations --- .../PaginatedTable/PaginatedTable.tsx | 37 ++++---- src/components/PaginatedTable/TableChunk.tsx | 21 ++--- .../PaginatedTable/useScrollBasedChunks.ts | 90 +++++++++++++------ src/containers/Nodes/NodesWrapper.tsx | 2 +- src/containers/Nodes/PaginatedNodes.tsx | 2 +- src/containers/PDiskPage/PDiskPage.tsx | 5 +- src/containers/Storage/PaginatedStorage.tsx | 2 +- .../Storage/PaginatedStorageGroups.tsx | 3 +- .../Storage/PaginatedStorageNodes.tsx | 2 + .../PaginatedStorageGroupsTable.tsx | 2 +- .../PaginatedStorageNodesTable.tsx | 2 +- src/containers/Storage/StorageWrapper.tsx | 2 +- .../StorageGroupPage/StorageGroupPage.tsx | 5 +- src/containers/VDiskPage/VDiskPage.tsx | 4 +- 14 files changed, 107 insertions(+), 72 deletions(-) diff --git a/src/components/PaginatedTable/PaginatedTable.tsx b/src/components/PaginatedTable/PaginatedTable.tsx index 057723f8f..5c3707037 100644 --- a/src/components/PaginatedTable/PaginatedTable.tsx +++ b/src/components/PaginatedTable/PaginatedTable.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import {getArray} from '../../utils'; import {TableWithControlsLayout} from '../TableWithControlsLayout/TableWithControlsLayout'; import {TableChunk} from './TableChunk'; @@ -32,7 +31,7 @@ export interface PaginatedTableProps { columns: Column[]; getRowClassName?: GetRowClassName; rowHeight?: number; - parentRef?: React.RefObject; + parentRef: React.RefObject | null; initialSortParams?: SortParams; onColumnsResize?: HandleTableColumnsResize; renderControls?: RenderControls; @@ -42,7 +41,7 @@ export interface PaginatedTableProps { } export const PaginatedTable = ({ - limit, + limit: chunkSize, initialEntitiesCount, fetchData, filters, @@ -58,8 +57,8 @@ export const PaginatedTable = ({ renderEmptyDataMessage, containerClassName, }: PaginatedTableProps) => { - const initialTotal = initialEntitiesCount || limit; - const initialFound = initialEntitiesCount || 0; + const initialTotal = initialEntitiesCount || 1; + const initialFound = initialEntitiesCount || 1; const [sortParams, setSortParams] = React.useState(initialSortParams); const [totalEntities, setTotalEntities] = React.useState(initialTotal); @@ -73,7 +72,7 @@ export const PaginatedTable = ({ tableRef, totalItems: foundEntities, rowHeight, - chunkSize: limit, + chunkSize, }); const handleDataFetched = React.useCallback( @@ -93,15 +92,13 @@ export const PaginatedTable = ({ // reset table on filters change React.useLayoutEffect(() => { + if (parentRef?.current && tableRef.current && !initialTotal) { + parentRef.current.scrollTo(0, tableRef.current.offsetTop); + } setTotalEntities(initialTotal); setFoundEntities(initialFound); setIsInitialLoad(true); - if (parentRef?.current) { - parentRef.current.scrollTo(0, 0); - } else { - tableRef.current?.scrollTo(0, 0); - } - }, [filters, initialFound, initialTotal, limit, parentRef]); + }, [filters, initialFound, initialTotal, parentRef]); const renderChunks = () => { if (!isInitialLoad && foundEntities === 0) { @@ -114,15 +111,13 @@ export const PaginatedTable = ({ ); } - const totalLength = foundEntities || limit; - const chunksCount = Math.ceil(totalLength / limit); - - return getArray(chunksCount).map((value) => ( + const totalItemsCount = foundEntities || chunkSize; + return activeChunks.map((isActive, index) => ( - key={value} - id={value} - limit={limit} - totalLength={totalLength} + key={index} + id={index} + totalItemsCount={totalItemsCount} + chunkSize={chunkSize} rowHeight={rowHeight} columns={columns} fetchData={fetchData} @@ -132,7 +127,7 @@ export const PaginatedTable = ({ getRowClassName={getRowClassName} renderErrorMessage={renderErrorMessage} onDataFetched={handleDataFetched} - isActive={activeChunks[value]} + isActive={isActive} /> )); }; diff --git a/src/components/PaginatedTable/TableChunk.tsx b/src/components/PaginatedTable/TableChunk.tsx index f48c47380..7f74b11f1 100644 --- a/src/components/PaginatedTable/TableChunk.tsx +++ b/src/components/PaginatedTable/TableChunk.tsx @@ -14,9 +14,9 @@ const DEBOUNCE_TIMEOUT = 200; interface TableChunkProps { id: number; - limit: number; - totalLength: number; + chunkSize: number; rowHeight: number; + totalItemsCount: number; columns: Column[]; filters?: F; sortParams?: SortParams; @@ -32,8 +32,8 @@ interface TableChunkProps { // Memoisation prevents chunks rerenders that could cause perfomance issues on big tables export const TableChunk = typedMemo(function TableChunk({ id, - limit, - totalLength, + chunkSize, + totalItemsCount, rowHeight, columns, fetchData, @@ -51,8 +51,8 @@ export const TableChunk = typedMemo(function TableChunk({ const columnsIds = columns.map((column) => column.name); const queryParams = { - offset: id * limit, - limit, + offset: id * chunkSize, + limit: chunkSize, fetchData: fetchData as FetchData, filters, sortParams, @@ -88,9 +88,10 @@ export const TableChunk = typedMemo(function TableChunk({ } }, [currentData, isActive, onDataFetched]); - const chunkOffset = id * limit; - const remainingLength = totalLength - chunkOffset; - const calculatedChunkLength = remainingLength < limit ? remainingLength : limit; + const chunkOffset = id * chunkSize; + const remainingLength = totalItemsCount - chunkOffset; + const calculatedChunkLength = + remainingLength < chunkSize && remainingLength > 0 ? remainingLength : chunkSize; const dataLength = currentData?.data?.length || calculatedChunkLength; @@ -135,7 +136,7 @@ export const TableChunk = typedMemo(function TableChunk({ )); }; - const chunkHeight = dataLength ? dataLength * rowHeight : limit * rowHeight; + const chunkHeight = dataLength ? dataLength * rowHeight : chunkSize * rowHeight; return ( ; + parentRef: React.RefObject | null; tableRef: React.RefObject; totalItems: number; rowHeight: number; chunkSize: number; + overscanCount?: number; } +interface ChunksRange { + start: number; + end: number; +} + +const DEFAULT_OVERSCAN_COUNT = 1; const THROTTLE_DELAY = 100; -const CHUNKS_AHEAD_COUNT = 1; export const useScrollBasedChunks = ({ parentRef, @@ -19,42 +25,55 @@ export const useScrollBasedChunks = ({ totalItems, rowHeight, chunkSize, + overscanCount = DEFAULT_OVERSCAN_COUNT, }: UseScrollBasedChunksProps): boolean[] => { - const [activeChunks, setActiveChunks] = React.useState([true, true]); + const chunksCount = React.useMemo( + () => Math.ceil(totalItems / chunkSize), + [chunkSize, totalItems], + ); - const calculateActiveChunks = React.useCallback(() => { + const [chunksRange, setChunksRange] = React.useState({ + start: 0, + end: Math.min(overscanCount, chunksCount - 1), + }); + + const calculateVisibleRange = React.useCallback(() => { const container = parentRef?.current; const table = tableRef.current; if (!container || !table) { - return; + return null; } - const tableScrollTop = Math.max(container.scrollTop - table.offsetTop, 0); - const visibleStartIndex = Math.floor(Math.max(tableScrollTop, 0) / rowHeight); - const visibleEndIndex = Math.min( - Math.ceil((tableScrollTop + container.clientHeight) / rowHeight), - totalItems - 1, - ); + const tableOffset = table.offsetTop; + const containerScroll = container.scrollTop; + const visibleStart = Math.max(containerScroll - tableOffset, 0); + const visibleEnd = visibleStart + container.clientHeight; const startChunk = Math.max( - Math.floor(visibleStartIndex / chunkSize) - CHUNKS_AHEAD_COUNT, + Math.floor(visibleStart / rowHeight / chunkSize) - overscanCount, 0, ); - const endChunk = Math.floor(visibleEndIndex / chunkSize) + CHUNKS_AHEAD_COUNT; + const endChunk = Math.min( + Math.floor(visibleEnd / rowHeight / chunkSize) + overscanCount, + chunksCount - 1, + ); - const newActiveChunks = []; - for (let i = startChunk; i <= endChunk; i++) { - newActiveChunks[i] = true; - } + return {start: startChunk, end: endChunk}; + }, [parentRef, tableRef, rowHeight, chunkSize, chunksCount, overscanCount]); - if (!isEqual(activeChunks, newActiveChunks)) { - setActiveChunks(newActiveChunks); + const handleScroll = React.useCallback(() => { + const newRange = calculateVisibleRange(); + if ( + newRange && + (newRange.start !== chunksRange.start || newRange.end !== chunksRange.end) + ) { + setChunksRange(newRange); } - }, [parentRef, tableRef, rowHeight, totalItems, chunkSize, activeChunks]); + }, [calculateVisibleRange, chunksRange.end, chunksRange.start]); - const throttledCalculateActiveChunks = React.useMemo( - () => throttle(calculateActiveChunks, THROTTLE_DELAY), - [calculateActiveChunks], + const throttledHandleScroll = React.useMemo( + () => throttle(handleScroll, THROTTLE_DELAY, {leading: true, trailing: true}), + [handleScroll], ); React.useEffect(() => { @@ -63,12 +82,25 @@ export const useScrollBasedChunks = ({ return undefined; } - container.addEventListener('scroll', throttledCalculateActiveChunks); + container.addEventListener('scroll', throttledHandleScroll); return () => { - container.removeEventListener('scroll', throttledCalculateActiveChunks); - throttledCalculateActiveChunks.cancel(); + container.removeEventListener('scroll', throttledHandleScroll); + throttledHandleScroll.cancel(); }; - }, [parentRef, throttledCalculateActiveChunks]); + }, [parentRef, throttledHandleScroll]); + + return React.useMemo(() => { + const activeChunkIds = Array.from( + {length: chunksRange.end - chunksRange.start + 1}, + (_, i) => chunksRange.start + i, + ); + + // Create boolean array where true represents active chunks + const activeChunks = Array(chunksCount).fill(false); + activeChunkIds.forEach((id) => { + activeChunks[id] = true; + }); - return activeChunks; + return activeChunks; + }, [chunksRange.start, chunksRange.end, chunksCount]); }; diff --git a/src/containers/Nodes/NodesWrapper.tsx b/src/containers/Nodes/NodesWrapper.tsx index 3cfcd212c..1a0648e7d 100644 --- a/src/containers/Nodes/NodesWrapper.tsx +++ b/src/containers/Nodes/NodesWrapper.tsx @@ -8,7 +8,7 @@ import {PaginatedNodes} from './PaginatedNodes'; interface NodesWrapperProps { path?: string; database?: string; - parentRef?: React.RefObject; + parentRef: React.RefObject | null; additionalNodesProps?: AdditionalNodesProps; } diff --git a/src/containers/Nodes/PaginatedNodes.tsx b/src/containers/Nodes/PaginatedNodes.tsx index 105552126..cc187ef84 100644 --- a/src/containers/Nodes/PaginatedNodes.tsx +++ b/src/containers/Nodes/PaginatedNodes.tsx @@ -44,7 +44,7 @@ const b = cn('ydb-nodes'); interface NodesProps { path?: string; database?: string; - parentRef?: React.RefObject; + parentRef: React.RefObject | null; additionalNodesProps?: AdditionalNodesProps; } diff --git a/src/containers/PDiskPage/PDiskPage.tsx b/src/containers/PDiskPage/PDiskPage.tsx index d07035b0d..435a5b7be 100644 --- a/src/containers/PDiskPage/PDiskPage.tsx +++ b/src/containers/PDiskPage/PDiskPage.tsx @@ -63,6 +63,7 @@ export function PDiskPage() { const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges); const newDiskApiAvailable = useDiskPagesAvailable(); + const containerRef = React.useRef(null); const [{nodeId, pDiskId, activeTab}] = useQueryParams({ activeTab: StringParam, @@ -245,7 +246,7 @@ export function PDiskPage() { } case 'storage': { return pDiskParamsDefined ? ( - + ) : null; } default: @@ -261,7 +262,7 @@ export function PDiskPage() { }; return ( -
+
{renderHelmet()} {renderPageMeta()} {renderPageTitle()} diff --git a/src/containers/Storage/PaginatedStorage.tsx b/src/containers/Storage/PaginatedStorage.tsx index 6a01ce895..784cd53ca 100644 --- a/src/containers/Storage/PaginatedStorage.tsx +++ b/src/containers/Storage/PaginatedStorage.tsx @@ -12,7 +12,7 @@ export interface PaginatedStorageProps { viewContext: StorageViewContext; - parentRef?: React.RefObject; + parentRef: React.RefObject | null; initialEntitiesCount?: number; } diff --git a/src/containers/Storage/PaginatedStorageGroups.tsx b/src/containers/Storage/PaginatedStorageGroups.tsx index 03735f3e1..b4dc1048c 100644 --- a/src/containers/Storage/PaginatedStorageGroups.tsx +++ b/src/containers/Storage/PaginatedStorageGroups.tsx @@ -104,7 +104,7 @@ function GroupedStorageGroupsComponent({ nodeId, groupId, pDiskId, - + parentRef, viewContext, }: PaginatedStorageProps) { const [autoRefreshInterval] = useAutoRefreshInterval(); @@ -167,6 +167,7 @@ function GroupedStorageGroupsComponent({ > ; + parentRef: React.RefObject | null; renderControls?: RenderControls; renderErrorMessage: RenderErrorMessage; initialEntitiesCount?: number; diff --git a/src/containers/Storage/StorageNodes/PaginatedStorageNodesTable.tsx b/src/containers/Storage/StorageNodes/PaginatedStorageNodesTable.tsx index 44985f5d6..6bc51c300 100644 --- a/src/containers/Storage/StorageNodes/PaginatedStorageNodesTable.tsx +++ b/src/containers/Storage/StorageNodes/PaginatedStorageNodesTable.tsx @@ -29,7 +29,7 @@ interface PaginatedStorageNodesTableProps { nodesUptimeFilter: NodesUptimeFilterValues; onShowAll: VoidFunction; - parentRef?: React.RefObject; + parentRef: React.RefObject | null; renderControls?: RenderControls; renderErrorMessage: RenderErrorMessage; initialEntitiesCount?: number; diff --git a/src/containers/Storage/StorageWrapper.tsx b/src/containers/Storage/StorageWrapper.tsx index d2c5a264e..6ebd58408 100644 --- a/src/containers/Storage/StorageWrapper.tsx +++ b/src/containers/Storage/StorageWrapper.tsx @@ -11,7 +11,7 @@ interface StorageWrapperProps { pDiskId?: string | number; groupId?: string | number; vDiskSlotId?: string | number; - parentRef?: React.RefObject; + parentRef: React.RefObject | null; } export const StorageWrapper = ({parentRef, ...props}: StorageWrapperProps) => { diff --git a/src/containers/StorageGroupPage/StorageGroupPage.tsx b/src/containers/StorageGroupPage/StorageGroupPage.tsx index 752b5eb72..68058f4ea 100644 --- a/src/containers/StorageGroupPage/StorageGroupPage.tsx +++ b/src/containers/StorageGroupPage/StorageGroupPage.tsx @@ -29,6 +29,7 @@ const storageGroupPageCn = cn('ydb-storage-group-page'); export function StorageGroupPage() { const dispatch = useTypedDispatch(); + const containerRef = React.useRef(null); const [{groupId}] = useQueryParams({groupId: StringParam}); @@ -109,7 +110,7 @@ export function StorageGroupPage() {
{storageGroupPageKeyset('storage')}
- + ); }; @@ -122,7 +123,7 @@ export function StorageGroupPage() { }; return ( -
+
{renderHelmet()} {renderPageMeta()} {renderPageTitle()} diff --git a/src/containers/VDiskPage/VDiskPage.tsx b/src/containers/VDiskPage/VDiskPage.tsx index ab6249cd6..f98d4e341 100644 --- a/src/containers/VDiskPage/VDiskPage.tsx +++ b/src/containers/VDiskPage/VDiskPage.tsx @@ -32,6 +32,7 @@ const vDiskPageCn = cn('ydb-vdisk-page'); export function VDiskPage() { const dispatch = useTypedDispatch(); + const containerRef = React.useRef(null); const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges); const newDiskApiAvailable = useDiskPagesAvailable(); @@ -182,6 +183,7 @@ export function VDiskPage() { nodeId={nodeId} pDiskId={pDiskId ?? undefined} vDiskSlotId={vDiskSlotId ?? undefined} + parentRef={containerRef} /> ); @@ -205,7 +207,7 @@ export function VDiskPage() { }; return ( -
+
{renderHelmet()} {renderPageMeta()} {renderPageTitle()} From 4e2c4ddfc99513d10afb7cc84d88011b37b26790 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Wed, 23 Oct 2024 19:56:24 +0300 Subject: [PATCH 04/21] feat: test data --- src/containers/Nodes/getNodes.ts | 21 ++++++++++++++---- .../Storage/StorageNodes/getNodes.ts | 22 +++++++++++++++---- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/containers/Nodes/getNodes.ts b/src/containers/Nodes/getNodes.ts index 7fd7a11ef..36f1620b8 100644 --- a/src/containers/Nodes/getNodes.ts +++ b/src/containers/Nodes/getNodes.ts @@ -46,7 +46,7 @@ export const getNodes: FetchData< storage, tablets, limit, - offset, + offset: 0, sort, path, database, @@ -59,9 +59,22 @@ export const getNodes: FetchData< ); const preparedResponse = prepareNodesData(response); + let mockedData = preparedResponse.Nodes?.slice(); + + for (let i = 0; i < 1000; i++) { + mockedData = mockedData?.concat( + preparedResponse.Nodes?.map((data, j) => ({ + ...data, + NodeId: data.NodeId + i * 2000 + j, + Host: data.Host || String(i) + ',' + j, + })) || [], + ); + } + const paginatedData = mockedData?.slice(offset, offset + limit); + return { - data: preparedResponse.Nodes || [], - found: preparedResponse.FoundNodes || 0, - total: preparedResponse.TotalNodes || 0, + data: paginatedData || [], + found: mockedData?.length || 0, + total: mockedData?.length || 0, }; }; diff --git a/src/containers/Storage/StorageNodes/getNodes.ts b/src/containers/Storage/StorageNodes/getNodes.ts index 86ca308b5..e717e2174 100644 --- a/src/containers/Storage/StorageNodes/getNodes.ts +++ b/src/containers/Storage/StorageNodes/getNodes.ts @@ -46,7 +46,7 @@ export const getStorageNodes: FetchData< type, storage, limit, - offset, + offset: 0, sort, filter: searchValue, uptime: getUptimeParamValue(nodesUptimeFilter), @@ -59,9 +59,23 @@ export const getStorageNodes: FetchData< fieldsRequired: dataFieldsRequired, }); const preparedResponse = prepareStorageNodesResponse(response); + + let mockedData = preparedResponse.nodes?.slice(); + + for (let i = 0; i < 1000; i++) { + mockedData = mockedData?.concat( + preparedResponse.nodes?.map((data, j) => ({ + ...data, + NodeId: data.NodeId + i * 2000 + j, + Host: data.Host || String(i) + ',' + j, + })) || [], + ); + } + const paginatedData = mockedData?.slice(offset, offset + limit); + return { - data: preparedResponse.nodes || [], - found: preparedResponse.found || 0, - total: preparedResponse.total || 0, + data: paginatedData || [], + found: mockedData?.length || 0, + total: mockedData?.length || 0, }; }; From 692aea876ae009823ffa353faba918b953f5c0f3 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 24 Oct 2024 15:30:14 +0300 Subject: [PATCH 05/21] fix: some refinements --- .../PaginatedTable/PaginatedTable.tsx | 30 +++++++++---------- .../PaginatedTable/useScrollBasedChunks.ts | 4 ++- src/components/PaginatedTable/utils.tsx | 12 ++++++++ 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/components/PaginatedTable/PaginatedTable.tsx b/src/components/PaginatedTable/PaginatedTable.tsx index 5c3707037..88fe5c4ab 100644 --- a/src/components/PaginatedTable/PaginatedTable.tsx +++ b/src/components/PaginatedTable/PaginatedTable.tsx @@ -19,9 +19,13 @@ import type { SortParams, } from './types'; import {useScrollBasedChunks} from './useScrollBasedChunks'; +import {calculateElementOffsetTop} from './utils'; import './PaginatedTable.scss'; +const HEADER_HEIGHT = 40; +const CONTROLS_HEIGHT = 50; + export interface PaginatedTableProps { limit: number; initialEntitiesCount?: number; @@ -57,7 +61,7 @@ export const PaginatedTable = ({ renderEmptyDataMessage, containerClassName, }: PaginatedTableProps) => { - const initialTotal = initialEntitiesCount || 1; + const initialTotal = initialEntitiesCount || 0; const initialFound = initialEntitiesCount || 1; const [sortParams, setSortParams] = React.useState(initialSortParams); @@ -75,25 +79,19 @@ export const PaginatedTable = ({ chunkSize, }); - const handleDataFetched = React.useCallback( - (total: number, found: number) => { - if (total !== totalEntities) { - setTotalEntities(total); - } - - if (found !== foundEntities) { - setFoundEntities(found); - } - - setIsInitialLoad(false); - }, - [foundEntities, totalEntities], - ); + const handleDataFetched = React.useCallback((total: number, found: number) => { + setTotalEntities(total); + setFoundEntities(found); + setIsInitialLoad(false); + }, []); // reset table on filters change React.useLayoutEffect(() => { if (parentRef?.current && tableRef.current && !initialTotal) { - parentRef.current.scrollTo(0, tableRef.current.offsetTop); + parentRef.current.scrollTo({ + left: 0, + top: calculateElementOffsetTop(tableRef.current) - HEADER_HEIGHT - CONTROLS_HEIGHT, + }); } setTotalEntities(initialTotal); setFoundEntities(initialFound); diff --git a/src/components/PaginatedTable/useScrollBasedChunks.ts b/src/components/PaginatedTable/useScrollBasedChunks.ts index ee35f6982..24800377a 100644 --- a/src/components/PaginatedTable/useScrollBasedChunks.ts +++ b/src/components/PaginatedTable/useScrollBasedChunks.ts @@ -2,6 +2,8 @@ import React from 'react'; import {throttle} from 'lodash'; +import {calculateElementOffsetTop} from './utils'; + interface UseScrollBasedChunksProps { parentRef: React.RefObject | null; tableRef: React.RefObject; @@ -44,7 +46,7 @@ export const useScrollBasedChunks = ({ return null; } - const tableOffset = table.offsetTop; + const tableOffset = calculateElementOffsetTop(table, container); const containerScroll = container.scrollTop; const visibleStart = Math.max(containerScroll - tableOffset, 0); const visibleEnd = visibleStart + container.clientHeight; diff --git a/src/components/PaginatedTable/utils.tsx b/src/components/PaginatedTable/utils.tsx index d9fb6bb3b..2015583aa 100644 --- a/src/components/PaginatedTable/utils.tsx +++ b/src/components/PaginatedTable/utils.tsx @@ -27,3 +27,15 @@ export function calculateColumnWidth(newWidth: number, minWidth = 40, maxWidth = } export const typedMemo: (Component: T) => T = React.memo; + +export function calculateElementOffsetTop(element: HTMLElement, container?: HTMLElement): number { + let currentElement = element; + let offsetTop = 0; + + while (currentElement && currentElement !== container) { + offsetTop += currentElement.offsetTop; + currentElement = currentElement.offsetParent as HTMLElement; + } + + return offsetTop; +} From 75774c54b5b860f9bb65adcb5fb006b0ce1e1eda Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 24 Oct 2024 15:37:38 +0300 Subject: [PATCH 06/21] fix: green test --- tests/suites/nodes/nodes.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/suites/nodes/nodes.test.ts b/tests/suites/nodes/nodes.test.ts index fd895e466..c024a1b2f 100644 --- a/tests/suites/nodes/nodes.test.ts +++ b/tests/suites/nodes/nodes.test.ts @@ -83,9 +83,8 @@ test.describe('Test Nodes Paginated Table', async () => { await paginatedTable.waitForTableData(); const nodeCount = await paginatedTable.getCount(); - const rowCount = await paginatedTable.getRowCount(); - expect(nodeCount).toBe(rowCount); + expect(nodeCount).toBe(1001); }); test('Uptime values are displayed in correct format', async ({page}) => { From 798cf0f4bb0527049d9b32331a214a6f1b86a380 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 24 Oct 2024 15:50:39 +0300 Subject: [PATCH 07/21] fix: Nodes page scrolling --- src/containers/Node/Node.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containers/Node/Node.tsx b/src/containers/Node/Node.tsx index 35b237e1b..7424d777c 100644 --- a/src/containers/Node/Node.tsx +++ b/src/containers/Node/Node.tsx @@ -126,7 +126,7 @@ export function Node(props: NodeProps) { switch (activeTabVerified.id) { case STORAGE: { return ( -
+
); @@ -159,7 +159,7 @@ export function Node(props: NodeProps) { if (node) { return ( -
+
Date: Thu, 24 Oct 2024 16:39:31 +0300 Subject: [PATCH 08/21] fix: throttling --- .../PaginatedTable/useScrollBasedChunks.ts | 54 ++++++++----------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/src/components/PaginatedTable/useScrollBasedChunks.ts b/src/components/PaginatedTable/useScrollBasedChunks.ts index 24800377a..acb27a86e 100644 --- a/src/components/PaginatedTable/useScrollBasedChunks.ts +++ b/src/components/PaginatedTable/useScrollBasedChunks.ts @@ -13,11 +13,6 @@ interface UseScrollBasedChunksProps { overscanCount?: number; } -interface ChunksRange { - start: number; - end: number; -} - const DEFAULT_OVERSCAN_COUNT = 1; const THROTTLE_DELAY = 100; @@ -34,10 +29,10 @@ export const useScrollBasedChunks = ({ [chunkSize, totalItems], ); - const [chunksRange, setChunksRange] = React.useState({ - start: 0, - end: Math.min(overscanCount, chunksCount - 1), - }); + const [startChunk, setStartChunk] = React.useState(0); + const [endChunk, setEndChunk] = React.useState( + Math.min(overscanCount, Math.max(chunksCount - 1, 0)), + ); const calculateVisibleRange = React.useCallback(() => { const container = parentRef?.current; @@ -51,32 +46,22 @@ export const useScrollBasedChunks = ({ const visibleStart = Math.max(containerScroll - tableOffset, 0); const visibleEnd = visibleStart + container.clientHeight; - const startChunk = Math.max( - Math.floor(visibleStart / rowHeight / chunkSize) - overscanCount, - 0, - ); - const endChunk = Math.min( + const start = Math.max(Math.floor(visibleStart / rowHeight / chunkSize) - overscanCount, 0); + const end = Math.min( Math.floor(visibleEnd / rowHeight / chunkSize) + overscanCount, - chunksCount - 1, + Math.max(chunksCount - 1, 0), ); - return {start: startChunk, end: endChunk}; - }, [parentRef, tableRef, rowHeight, chunkSize, chunksCount, overscanCount]); + return {start, end}; + }, [parentRef, tableRef, rowHeight, chunkSize, overscanCount, chunksCount]); const handleScroll = React.useCallback(() => { const newRange = calculateVisibleRange(); - if ( - newRange && - (newRange.start !== chunksRange.start || newRange.end !== chunksRange.end) - ) { - setChunksRange(newRange); + if (newRange) { + setStartChunk(newRange.start); + setEndChunk(newRange.end); } - }, [calculateVisibleRange, chunksRange.end, chunksRange.start]); - - const throttledHandleScroll = React.useMemo( - () => throttle(handleScroll, THROTTLE_DELAY, {leading: true, trailing: true}), - [handleScroll], - ); + }, [calculateVisibleRange]); React.useEffect(() => { const container = parentRef?.current; @@ -84,17 +69,22 @@ export const useScrollBasedChunks = ({ return undefined; } + const throttledHandleScroll = throttle(handleScroll, THROTTLE_DELAY, { + leading: true, + trailing: true, + }); + container.addEventListener('scroll', throttledHandleScroll); return () => { container.removeEventListener('scroll', throttledHandleScroll); throttledHandleScroll.cancel(); }; - }, [parentRef, throttledHandleScroll]); + }, [handleScroll, parentRef]); return React.useMemo(() => { const activeChunkIds = Array.from( - {length: chunksRange.end - chunksRange.start + 1}, - (_, i) => chunksRange.start + i, + {length: endChunk - startChunk + 1}, + (_, i) => startChunk + i, ); // Create boolean array where true represents active chunks @@ -104,5 +94,5 @@ export const useScrollBasedChunks = ({ }); return activeChunks; - }, [chunksRange.start, chunksRange.end, chunksCount]); + }, [endChunk, startChunk, chunksCount]); }; From 4494e98ff2aa5c0dfdcec4e90002d60d17a6b32b Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 24 Oct 2024 18:12:30 +0300 Subject: [PATCH 09/21] fix: parentRef cant be null --- src/components/PaginatedTable/PaginatedTable.tsx | 2 +- .../PaginatedTable/useScrollBasedChunks.ts | 13 ++++--------- src/containers/Nodes/NodesWrapper.tsx | 2 +- src/containers/Nodes/PaginatedNodes.tsx | 2 +- src/containers/Storage/PaginatedStorage.tsx | 2 +- .../StorageGroups/PaginatedStorageGroupsTable.tsx | 2 +- .../StorageNodes/PaginatedStorageNodesTable.tsx | 2 +- src/containers/Storage/StorageWrapper.tsx | 2 +- 8 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/components/PaginatedTable/PaginatedTable.tsx b/src/components/PaginatedTable/PaginatedTable.tsx index 88fe5c4ab..f13aa49cb 100644 --- a/src/components/PaginatedTable/PaginatedTable.tsx +++ b/src/components/PaginatedTable/PaginatedTable.tsx @@ -35,7 +35,7 @@ export interface PaginatedTableProps { columns: Column[]; getRowClassName?: GetRowClassName; rowHeight?: number; - parentRef: React.RefObject | null; + parentRef: React.RefObject; initialSortParams?: SortParams; onColumnsResize?: HandleTableColumnsResize; renderControls?: RenderControls; diff --git a/src/components/PaginatedTable/useScrollBasedChunks.ts b/src/components/PaginatedTable/useScrollBasedChunks.ts index acb27a86e..6b8b1ce45 100644 --- a/src/components/PaginatedTable/useScrollBasedChunks.ts +++ b/src/components/PaginatedTable/useScrollBasedChunks.ts @@ -5,7 +5,7 @@ import {throttle} from 'lodash'; import {calculateElementOffsetTop} from './utils'; interface UseScrollBasedChunksProps { - parentRef: React.RefObject | null; + parentRef: React.RefObject; tableRef: React.RefObject; totalItems: number; rowHeight: number; @@ -82,16 +82,11 @@ export const useScrollBasedChunks = ({ }, [handleScroll, parentRef]); return React.useMemo(() => { - const activeChunkIds = Array.from( - {length: endChunk - startChunk + 1}, - (_, i) => startChunk + i, - ); - // Create boolean array where true represents active chunks const activeChunks = Array(chunksCount).fill(false); - activeChunkIds.forEach((id) => { - activeChunks[id] = true; - }); + for (let i = startChunk; i <= endChunk; i++) { + activeChunks[i] = true; + } return activeChunks; }, [endChunk, startChunk, chunksCount]); diff --git a/src/containers/Nodes/NodesWrapper.tsx b/src/containers/Nodes/NodesWrapper.tsx index 1a0648e7d..aef41e265 100644 --- a/src/containers/Nodes/NodesWrapper.tsx +++ b/src/containers/Nodes/NodesWrapper.tsx @@ -8,7 +8,7 @@ import {PaginatedNodes} from './PaginatedNodes'; interface NodesWrapperProps { path?: string; database?: string; - parentRef: React.RefObject | null; + parentRef: React.RefObject; additionalNodesProps?: AdditionalNodesProps; } diff --git a/src/containers/Nodes/PaginatedNodes.tsx b/src/containers/Nodes/PaginatedNodes.tsx index cc187ef84..a0966ede0 100644 --- a/src/containers/Nodes/PaginatedNodes.tsx +++ b/src/containers/Nodes/PaginatedNodes.tsx @@ -44,7 +44,7 @@ const b = cn('ydb-nodes'); interface NodesProps { path?: string; database?: string; - parentRef: React.RefObject | null; + parentRef: React.RefObject; additionalNodesProps?: AdditionalNodesProps; } diff --git a/src/containers/Storage/PaginatedStorage.tsx b/src/containers/Storage/PaginatedStorage.tsx index 784cd53ca..8ec205bc5 100644 --- a/src/containers/Storage/PaginatedStorage.tsx +++ b/src/containers/Storage/PaginatedStorage.tsx @@ -12,7 +12,7 @@ export interface PaginatedStorageProps { viewContext: StorageViewContext; - parentRef: React.RefObject | null; + parentRef: React.RefObject; initialEntitiesCount?: number; } diff --git a/src/containers/Storage/StorageGroups/PaginatedStorageGroupsTable.tsx b/src/containers/Storage/StorageGroups/PaginatedStorageGroupsTable.tsx index b34bc547a..6f69ef278 100644 --- a/src/containers/Storage/StorageGroups/PaginatedStorageGroupsTable.tsx +++ b/src/containers/Storage/StorageGroups/PaginatedStorageGroupsTable.tsx @@ -32,7 +32,7 @@ interface PaginatedStorageGroupsTableProps { visibleEntities: VisibleEntities; onShowAll: VoidFunction; - parentRef: React.RefObject | null; + parentRef: React.RefObject; renderControls?: RenderControls; renderErrorMessage: RenderErrorMessage; initialEntitiesCount?: number; diff --git a/src/containers/Storage/StorageNodes/PaginatedStorageNodesTable.tsx b/src/containers/Storage/StorageNodes/PaginatedStorageNodesTable.tsx index 6bc51c300..be768a838 100644 --- a/src/containers/Storage/StorageNodes/PaginatedStorageNodesTable.tsx +++ b/src/containers/Storage/StorageNodes/PaginatedStorageNodesTable.tsx @@ -29,7 +29,7 @@ interface PaginatedStorageNodesTableProps { nodesUptimeFilter: NodesUptimeFilterValues; onShowAll: VoidFunction; - parentRef: React.RefObject | null; + parentRef: React.RefObject; renderControls?: RenderControls; renderErrorMessage: RenderErrorMessage; initialEntitiesCount?: number; diff --git a/src/containers/Storage/StorageWrapper.tsx b/src/containers/Storage/StorageWrapper.tsx index 6ebd58408..5b1b7120b 100644 --- a/src/containers/Storage/StorageWrapper.tsx +++ b/src/containers/Storage/StorageWrapper.tsx @@ -11,7 +11,7 @@ interface StorageWrapperProps { pDiskId?: string | number; groupId?: string | number; vDiskSlotId?: string | number; - parentRef: React.RefObject | null; + parentRef: React.RefObject; } export const StorageWrapper = ({parentRef, ...props}: StorageWrapperProps) => { From f96ce3eb23231fcbc2ca3b814131b8c43265cbe1 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 24 Oct 2024 20:04:02 +0300 Subject: [PATCH 10/21] fix: small refactor --- .../PaginatedTable/PaginatedTable.tsx | 9 ++++---- src/components/PaginatedTable/TableChunk.tsx | 15 ++++--------- src/components/PaginatedTable/constants.ts | 2 +- .../PaginatedTable/useScrollBasedChunks.ts | 22 ++++++++++++++----- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/components/PaginatedTable/PaginatedTable.tsx b/src/components/PaginatedTable/PaginatedTable.tsx index f13aa49cb..c47154660 100644 --- a/src/components/PaginatedTable/PaginatedTable.tsx +++ b/src/components/PaginatedTable/PaginatedTable.tsx @@ -71,7 +71,7 @@ export const PaginatedTable = ({ const tableRef = React.useRef(null); - const activeChunks = useScrollBasedChunks({ + const chunks = useScrollBasedChunks({ parentRef, tableRef, totalItems: foundEntities, @@ -109,12 +109,11 @@ export const PaginatedTable = ({ ); } - const totalItemsCount = foundEntities || chunkSize; - return activeChunks.map((isActive, index) => ( + return chunks.map((itemsCount, index) => ( key={index} id={index} - totalItemsCount={totalItemsCount} + itemsCount={itemsCount} chunkSize={chunkSize} rowHeight={rowHeight} columns={columns} @@ -125,7 +124,7 @@ export const PaginatedTable = ({ getRowClassName={getRowClassName} renderErrorMessage={renderErrorMessage} onDataFetched={handleDataFetched} - isActive={isActive} + isActive={Boolean(itemsCount)} /> )); }; diff --git a/src/components/PaginatedTable/TableChunk.tsx b/src/components/PaginatedTable/TableChunk.tsx index 7f74b11f1..bd357809b 100644 --- a/src/components/PaginatedTable/TableChunk.tsx +++ b/src/components/PaginatedTable/TableChunk.tsx @@ -16,7 +16,7 @@ interface TableChunkProps { id: number; chunkSize: number; rowHeight: number; - totalItemsCount: number; + itemsCount: number; columns: Column[]; filters?: F; sortParams?: SortParams; @@ -33,7 +33,7 @@ interface TableChunkProps { export const TableChunk = typedMemo(function TableChunk({ id, chunkSize, - totalItemsCount, + itemsCount, rowHeight, columns, fetchData, @@ -88,12 +88,7 @@ export const TableChunk = typedMemo(function TableChunk({ } }, [currentData, isActive, onDataFetched]); - const chunkOffset = id * chunkSize; - const remainingLength = totalItemsCount - chunkOffset; - const calculatedChunkLength = - remainingLength < chunkSize && remainingLength > 0 ? remainingLength : chunkSize; - - const dataLength = currentData?.data?.length || calculatedChunkLength; + const dataLength = currentData?.data?.length || itemsCount || chunkSize; const renderContent = () => { if (!isActive) { @@ -136,13 +131,11 @@ export const TableChunk = typedMemo(function TableChunk({ )); }; - const chunkHeight = dataLength ? dataLength * rowHeight : chunkSize * rowHeight; - return ( { +}: UseScrollBasedChunksProps): number[] => { const chunksCount = React.useMemo( () => Math.ceil(totalItems / chunkSize), [chunkSize, totalItems], ); + const chunkLengths = React.useMemo( + () => + getArray(chunksCount).map((value) => + Math.min(chunkSize, totalItems - value * chunkSize), + ), + [chunkSize, chunksCount, totalItems], + ); + const [startChunk, setStartChunk] = React.useState(0); const [endChunk, setEndChunk] = React.useState( Math.min(overscanCount, Math.max(chunksCount - 1, 0)), @@ -82,12 +92,12 @@ export const useScrollBasedChunks = ({ }, [handleScroll, parentRef]); return React.useMemo(() => { - // Create boolean array where true represents active chunks - const activeChunks = Array(chunksCount).fill(false); + // 0 items represent inactive chunk + const chunks = Array(chunksCount).fill(0); for (let i = startChunk; i <= endChunk; i++) { - activeChunks[i] = true; + chunks[i] = chunkLengths[i]; } - return activeChunks; - }, [endChunk, startChunk, chunksCount]); + return chunks; + }, [chunksCount, startChunk, endChunk, chunkLengths]); }; From f6d75f66313c0026b6e93178bd3b26b16c94c740 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 24 Oct 2024 20:13:33 +0300 Subject: [PATCH 11/21] fix: optimize calculation --- .../PaginatedTable/useScrollBasedChunks.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/components/PaginatedTable/useScrollBasedChunks.ts b/src/components/PaginatedTable/useScrollBasedChunks.ts index c663bbe9b..f53580ebb 100644 --- a/src/components/PaginatedTable/useScrollBasedChunks.ts +++ b/src/components/PaginatedTable/useScrollBasedChunks.ts @@ -2,8 +2,6 @@ import React from 'react'; import {throttle} from 'lodash'; -import {getArray} from '../../utils'; - import {calculateElementOffsetTop} from './utils'; interface UseScrollBasedChunksProps { @@ -31,14 +29,6 @@ export const useScrollBasedChunks = ({ [chunkSize, totalItems], ); - const chunkLengths = React.useMemo( - () => - getArray(chunksCount).map((value) => - Math.min(chunkSize, totalItems - value * chunkSize), - ), - [chunkSize, chunksCount, totalItems], - ); - const [startChunk, setStartChunk] = React.useState(0); const [endChunk, setEndChunk] = React.useState( Math.min(overscanCount, Math.max(chunksCount - 1, 0)), @@ -95,9 +85,9 @@ export const useScrollBasedChunks = ({ // 0 items represent inactive chunk const chunks = Array(chunksCount).fill(0); for (let i = startChunk; i <= endChunk; i++) { - chunks[i] = chunkLengths[i]; + chunks[i] = endChunk === chunksCount - 1 ? totalItems % chunkSize : chunkSize; } return chunks; - }, [chunksCount, startChunk, endChunk, chunkLengths]); + }, [chunksCount, startChunk, endChunk, totalItems, chunkSize]); }; From 27f7aad3506d0618aacea984bbc8a529fae7df11 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 24 Oct 2024 20:14:32 +0300 Subject: [PATCH 12/21] fix: nanofix --- src/components/PaginatedTable/useScrollBasedChunks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PaginatedTable/useScrollBasedChunks.ts b/src/components/PaginatedTable/useScrollBasedChunks.ts index f53580ebb..b4b913e1f 100644 --- a/src/components/PaginatedTable/useScrollBasedChunks.ts +++ b/src/components/PaginatedTable/useScrollBasedChunks.ts @@ -85,7 +85,7 @@ export const useScrollBasedChunks = ({ // 0 items represent inactive chunk const chunks = Array(chunksCount).fill(0); for (let i = startChunk; i <= endChunk; i++) { - chunks[i] = endChunk === chunksCount - 1 ? totalItems % chunkSize : chunkSize; + chunks[i] = i === chunksCount - 1 ? totalItems % chunkSize : chunkSize; } return chunks; From d7560c0fde15c838d57c705ed8e2f691bb6442df Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 24 Oct 2024 20:18:21 +0300 Subject: [PATCH 13/21] fix: optimize --- src/components/PaginatedTable/useScrollBasedChunks.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/PaginatedTable/useScrollBasedChunks.ts b/src/components/PaginatedTable/useScrollBasedChunks.ts index b4b913e1f..f5eac8a5e 100644 --- a/src/components/PaginatedTable/useScrollBasedChunks.ts +++ b/src/components/PaginatedTable/useScrollBasedChunks.ts @@ -84,10 +84,12 @@ export const useScrollBasedChunks = ({ return React.useMemo(() => { // 0 items represent inactive chunk const chunks = Array(chunksCount).fill(0); - for (let i = startChunk; i <= endChunk; i++) { - chunks[i] = i === chunksCount - 1 ? totalItems % chunkSize : chunkSize; + for (let i = startChunk; i < endChunk; i++) { + chunks[i] = chunkSize; } + chunks[endChunk] = endChunk === chunksCount - 1 ? totalItems % chunkSize : chunkSize; + return chunks; }, [chunksCount, startChunk, endChunk, totalItems, chunkSize]); }; From 35d54a33c61cc3a6c4d84afd28ff2f6f85c6a040 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 24 Oct 2024 22:41:11 +0300 Subject: [PATCH 14/21] fix: last chunk calc --- src/components/PaginatedTable/useScrollBasedChunks.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/PaginatedTable/useScrollBasedChunks.ts b/src/components/PaginatedTable/useScrollBasedChunks.ts index f5eac8a5e..950a636ca 100644 --- a/src/components/PaginatedTable/useScrollBasedChunks.ts +++ b/src/components/PaginatedTable/useScrollBasedChunks.ts @@ -88,7 +88,8 @@ export const useScrollBasedChunks = ({ chunks[i] = chunkSize; } - chunks[endChunk] = endChunk === chunksCount - 1 ? totalItems % chunkSize : chunkSize; + const lastChunkSize = totalItems % chunkSize || chunkSize; + chunks[endChunk] = endChunk === chunksCount - 1 ? lastChunkSize : chunkSize; return chunks; }, [chunksCount, startChunk, endChunk, totalItems, chunkSize]); From 6fea4b9b6bcc445f2d530410ff74d1bc3c54e696 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Fri, 25 Oct 2024 11:49:04 +0300 Subject: [PATCH 15/21] fix: scroll to 0 --- src/components/PaginatedTable/PaginatedTable.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/components/PaginatedTable/PaginatedTable.tsx b/src/components/PaginatedTable/PaginatedTable.tsx index c47154660..baed247ed 100644 --- a/src/components/PaginatedTable/PaginatedTable.tsx +++ b/src/components/PaginatedTable/PaginatedTable.tsx @@ -19,13 +19,9 @@ import type { SortParams, } from './types'; import {useScrollBasedChunks} from './useScrollBasedChunks'; -import {calculateElementOffsetTop} from './utils'; import './PaginatedTable.scss'; -const HEADER_HEIGHT = 40; -const CONTROLS_HEIGHT = 50; - export interface PaginatedTableProps { limit: number; initialEntitiesCount?: number; @@ -87,15 +83,12 @@ export const PaginatedTable = ({ // reset table on filters change React.useLayoutEffect(() => { - if (parentRef?.current && tableRef.current && !initialTotal) { - parentRef.current.scrollTo({ - left: 0, - top: calculateElementOffsetTop(tableRef.current) - HEADER_HEIGHT - CONTROLS_HEIGHT, - }); - } setTotalEntities(initialTotal); setFoundEntities(initialFound); setIsInitialLoad(true); + if (parentRef?.current) { + parentRef.current.scrollTo(0, 0); + } }, [filters, initialFound, initialTotal, parentRef]); const renderChunks = () => { From 34b05a70ebe2348a2f0aab0b33b46f754e18d32d Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Fri, 25 Oct 2024 17:20:25 +0300 Subject: [PATCH 16/21] fix: painting optimizations --- src/components/PaginatedTable/PaginatedTable.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/PaginatedTable/PaginatedTable.scss b/src/components/PaginatedTable/PaginatedTable.scss index 707d21a2b..ccb35e6f3 100644 --- a/src/components/PaginatedTable/PaginatedTable.scss +++ b/src/components/PaginatedTable/PaginatedTable.scss @@ -29,6 +29,10 @@ position: relative; z-index: 1; + // Performance optimization for row hovering. + // it actually works. + transform: translateZ(0); + &:hover { background: var(--paginated-table-hover-color); } @@ -177,6 +181,7 @@ } &__row-skeleton::after { + animation: none; animation-delay: 200ms; } } From e7da69962cab7039ceae7eac9060e48a8a74841e Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Fri, 25 Oct 2024 20:10:21 +0300 Subject: [PATCH 17/21] fix: turn animation off --- src/components/PaginatedTable/PaginatedTable.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/PaginatedTable/PaginatedTable.scss b/src/components/PaginatedTable/PaginatedTable.scss index ccb35e6f3..a9060b723 100644 --- a/src/components/PaginatedTable/PaginatedTable.scss +++ b/src/components/PaginatedTable/PaginatedTable.scss @@ -181,7 +181,6 @@ } &__row-skeleton::after { - animation: none; - animation-delay: 200ms; + animation: none !important; } } From cfc350d79d567270a58289cde3a8f3dbc49d0703 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Mon, 28 Oct 2024 11:37:07 +0300 Subject: [PATCH 18/21] feat: turn on paginated tables by default --- src/services/settings.ts | 2 +- tests/suites/nodes/NodesPage.ts | 3 +-- tests/suites/storage/StoragePage.ts | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/services/settings.ts b/src/services/settings.ts index 978a26f52..069583036 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -36,7 +36,7 @@ export const DEFAULT_USER_SETTINGS = { [LAST_USED_QUERY_ACTION_KEY]: QUERY_ACTIONS.execute, [ASIDE_HEADER_COMPACT_KEY]: true, [PARTITIONS_HIDDEN_COLUMNS_KEY]: [], - [USE_PAGINATED_TABLES_KEY]: false, + [USE_PAGINATED_TABLES_KEY]: true, [USE_CLUSTER_BALANCER_AS_BACKEND_KEY]: true, [ENABLE_AUTOCOMPLETE]: true, [AUTOCOMPLETE_ON_ENTER]: true, diff --git a/tests/suites/nodes/NodesPage.ts b/tests/suites/nodes/NodesPage.ts index f163e3757..e81119d96 100644 --- a/tests/suites/nodes/NodesPage.ts +++ b/tests/suites/nodes/NodesPage.ts @@ -2,7 +2,6 @@ import type {Locator, Page} from '@playwright/test'; import {PageModel} from '../../models/PageModel'; import {nodesPage} from '../../utils/constants'; -import {selectContentTable} from '../../utils/selectContentTable'; export class NodesPage extends PageModel { readonly table: Locator; @@ -10,6 +9,6 @@ export class NodesPage extends PageModel { constructor(page: Page) { super(page, nodesPage); - this.table = selectContentTable(this.selector); + this.table = this.selector.locator('.ydb-paginated-table__table'); } } diff --git a/tests/suites/storage/StoragePage.ts b/tests/suites/storage/StoragePage.ts index 99e63df80..4ea8b3c14 100644 --- a/tests/suites/storage/StoragePage.ts +++ b/tests/suites/storage/StoragePage.ts @@ -2,7 +2,6 @@ import type {Locator, Page} from '@playwright/test'; import {PageModel} from '../../models/PageModel'; import {storagePage} from '../../utils/constants'; -import {selectContentTable} from '../../utils/selectContentTable'; type EntityType = 'Groups' | 'Nodes'; @@ -15,7 +14,7 @@ export class StoragePage extends PageModel { constructor(page: Page) { super(page, storagePage); - this.table = selectContentTable(this.selector); + this.table = this.selector.locator('.ydb-paginated-table__table'); this.entityTypeSelector = this.selector.getByTestId(storageTypeFilterQa); } From 3d459a848a3e2c83e24b68559e25c045d58ad58e Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Mon, 28 Oct 2024 15:15:01 +0300 Subject: [PATCH 19/21] fix: review fixes --- .../PaginatedTable/PaginatedTable.tsx | 13 +++++++++---- src/components/PaginatedTable/TableChunk.tsx | 6 +++--- .../PaginatedTable/useScrollBasedChunks.ts | 18 +++++++----------- src/components/PaginatedTable/utils.tsx | 16 ++++++++++++++++ 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/components/PaginatedTable/PaginatedTable.tsx b/src/components/PaginatedTable/PaginatedTable.tsx index baed247ed..ad794127f 100644 --- a/src/components/PaginatedTable/PaginatedTable.tsx +++ b/src/components/PaginatedTable/PaginatedTable.tsx @@ -67,7 +67,7 @@ export const PaginatedTable = ({ const tableRef = React.useRef(null); - const chunks = useScrollBasedChunks({ + const activeChunks = useScrollBasedChunks({ parentRef, tableRef, totalItems: foundEntities, @@ -75,6 +75,11 @@ export const PaginatedTable = ({ chunkSize, }); + const lastChunkSize = React.useMemo( + () => foundEntities % chunkSize || chunkSize, + [foundEntities, chunkSize], + ); + const handleDataFetched = React.useCallback((total: number, found: number) => { setTotalEntities(total); setFoundEntities(found); @@ -102,11 +107,11 @@ export const PaginatedTable = ({ ); } - return chunks.map((itemsCount, index) => ( + return activeChunks.map((isActive, index) => ( key={index} id={index} - itemsCount={itemsCount} + calculatedCount={index === activeChunks.length - 1 ? lastChunkSize : chunkSize} chunkSize={chunkSize} rowHeight={rowHeight} columns={columns} @@ -117,7 +122,7 @@ export const PaginatedTable = ({ getRowClassName={getRowClassName} renderErrorMessage={renderErrorMessage} onDataFetched={handleDataFetched} - isActive={Boolean(itemsCount)} + isActive={isActive} /> )); }; diff --git a/src/components/PaginatedTable/TableChunk.tsx b/src/components/PaginatedTable/TableChunk.tsx index bd357809b..347c11025 100644 --- a/src/components/PaginatedTable/TableChunk.tsx +++ b/src/components/PaginatedTable/TableChunk.tsx @@ -16,7 +16,7 @@ interface TableChunkProps { id: number; chunkSize: number; rowHeight: number; - itemsCount: number; + calculatedCount: number; columns: Column[]; filters?: F; sortParams?: SortParams; @@ -33,7 +33,7 @@ interface TableChunkProps { export const TableChunk = typedMemo(function TableChunk({ id, chunkSize, - itemsCount, + calculatedCount, rowHeight, columns, fetchData, @@ -88,7 +88,7 @@ export const TableChunk = typedMemo(function TableChunk({ } }, [currentData, isActive, onDataFetched]); - const dataLength = currentData?.data?.length || itemsCount || chunkSize; + const dataLength = currentData?.data?.length || calculatedCount; const renderContent = () => { if (!isActive) { diff --git a/src/components/PaginatedTable/useScrollBasedChunks.ts b/src/components/PaginatedTable/useScrollBasedChunks.ts index 950a636ca..e44dc1768 100644 --- a/src/components/PaginatedTable/useScrollBasedChunks.ts +++ b/src/components/PaginatedTable/useScrollBasedChunks.ts @@ -23,7 +23,7 @@ export const useScrollBasedChunks = ({ rowHeight, chunkSize, overscanCount = DEFAULT_OVERSCAN_COUNT, -}: UseScrollBasedChunksProps): number[] => { +}: UseScrollBasedChunksProps): boolean[] => { const chunksCount = React.useMemo( () => Math.ceil(totalItems / chunkSize), [chunkSize, totalItems], @@ -82,15 +82,11 @@ export const useScrollBasedChunks = ({ }, [handleScroll, parentRef]); return React.useMemo(() => { - // 0 items represent inactive chunk - const chunks = Array(chunksCount).fill(0); - for (let i = startChunk; i < endChunk; i++) { - chunks[i] = chunkSize; + // boolean array that represents active chunks + const activeChunks = Array(chunksCount).fill(false); + for (let i = startChunk; i <= endChunk; i++) { + activeChunks[i] = true; } - - const lastChunkSize = totalItems % chunkSize || chunkSize; - chunks[endChunk] = endChunk === chunksCount - 1 ? lastChunkSize : chunkSize; - - return chunks; - }, [chunksCount, startChunk, endChunk, totalItems, chunkSize]); + return activeChunks; + }, [chunksCount, startChunk, endChunk]); }; diff --git a/src/components/PaginatedTable/utils.tsx b/src/components/PaginatedTable/utils.tsx index 2015583aa..f272ae159 100644 --- a/src/components/PaginatedTable/utils.tsx +++ b/src/components/PaginatedTable/utils.tsx @@ -28,6 +28,22 @@ export function calculateColumnWidth(newWidth: number, minWidth = 40, maxWidth = export const typedMemo: (Component: T) => T = React.memo; +/** + * Calculates the total vertical offset (distance from top) of an element relative to its container + * or the document body if no container is specified. + * + * This function traverses up through the DOM tree, accumulating offsetTop values + * from each parent element until it reaches either the specified container or + * the top of the document. + * + * @param element - The HTML element to calculate the offset for + * @param container - Optional container element to stop the calculation at + * @returns The total vertical offset in pixels + * + * Example: + * const offset = calculateElementOffsetTop(myElement, myContainer); + * // Returns the distance in pixels from myElement to the top of myContainer + */ export function calculateElementOffsetTop(element: HTMLElement, container?: HTMLElement): number { let currentElement = element; let offsetTop = 0; From 0b47dbc783008ab2873f495a4ea315d7fc7d7ee4 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Mon, 28 Oct 2024 16:34:25 +0300 Subject: [PATCH 20/21] fix: remove test data --- src/containers/Nodes/getNodes.ts | 21 ++++-------------- .../Storage/StorageNodes/getNodes.ts | 22 ++++--------------- 2 files changed, 8 insertions(+), 35 deletions(-) diff --git a/src/containers/Nodes/getNodes.ts b/src/containers/Nodes/getNodes.ts index 36f1620b8..7fd7a11ef 100644 --- a/src/containers/Nodes/getNodes.ts +++ b/src/containers/Nodes/getNodes.ts @@ -46,7 +46,7 @@ export const getNodes: FetchData< storage, tablets, limit, - offset: 0, + offset, sort, path, database, @@ -59,22 +59,9 @@ export const getNodes: FetchData< ); const preparedResponse = prepareNodesData(response); - let mockedData = preparedResponse.Nodes?.slice(); - - for (let i = 0; i < 1000; i++) { - mockedData = mockedData?.concat( - preparedResponse.Nodes?.map((data, j) => ({ - ...data, - NodeId: data.NodeId + i * 2000 + j, - Host: data.Host || String(i) + ',' + j, - })) || [], - ); - } - const paginatedData = mockedData?.slice(offset, offset + limit); - return { - data: paginatedData || [], - found: mockedData?.length || 0, - total: mockedData?.length || 0, + data: preparedResponse.Nodes || [], + found: preparedResponse.FoundNodes || 0, + total: preparedResponse.TotalNodes || 0, }; }; diff --git a/src/containers/Storage/StorageNodes/getNodes.ts b/src/containers/Storage/StorageNodes/getNodes.ts index e717e2174..86ca308b5 100644 --- a/src/containers/Storage/StorageNodes/getNodes.ts +++ b/src/containers/Storage/StorageNodes/getNodes.ts @@ -46,7 +46,7 @@ export const getStorageNodes: FetchData< type, storage, limit, - offset: 0, + offset, sort, filter: searchValue, uptime: getUptimeParamValue(nodesUptimeFilter), @@ -59,23 +59,9 @@ export const getStorageNodes: FetchData< fieldsRequired: dataFieldsRequired, }); const preparedResponse = prepareStorageNodesResponse(response); - - let mockedData = preparedResponse.nodes?.slice(); - - for (let i = 0; i < 1000; i++) { - mockedData = mockedData?.concat( - preparedResponse.nodes?.map((data, j) => ({ - ...data, - NodeId: data.NodeId + i * 2000 + j, - Host: data.Host || String(i) + ',' + j, - })) || [], - ); - } - const paginatedData = mockedData?.slice(offset, offset + limit); - return { - data: paginatedData || [], - found: mockedData?.length || 0, - total: mockedData?.length || 0, + data: preparedResponse.nodes || [], + found: preparedResponse.found || 0, + total: preparedResponse.total || 0, }; }; From 6eab133b38f2cc444b020be124f039f0ac96f854 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Mon, 28 Oct 2024 16:36:49 +0300 Subject: [PATCH 21/21] fix: tests --- tests/suites/nodes/nodes.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/suites/nodes/nodes.test.ts b/tests/suites/nodes/nodes.test.ts index c024a1b2f..fd895e466 100644 --- a/tests/suites/nodes/nodes.test.ts +++ b/tests/suites/nodes/nodes.test.ts @@ -83,8 +83,9 @@ test.describe('Test Nodes Paginated Table', async () => { await paginatedTable.waitForTableData(); const nodeCount = await paginatedTable.getCount(); + const rowCount = await paginatedTable.getRowCount(); - expect(nodeCount).toBe(1001); + expect(nodeCount).toBe(rowCount); }); test('Uptime values are displayed in correct format', async ({page}) => {