diff --git a/src/components/PaginatedTable/PaginatedTable.tsx b/src/components/PaginatedTable/PaginatedTable.tsx index ab31333034..f874d4a550 100644 --- a/src/components/PaginatedTable/PaginatedTable.tsx +++ b/src/components/PaginatedTable/PaginatedTable.tsx @@ -19,7 +19,7 @@ import type { RenderErrorMessage, SortParams, } from './types'; -import {useIntersectionObserver} from './useIntersectionObserver'; +import {useScrollBasedChunks} from './useScrollBasedChunks'; import './PaginatedTable.scss'; @@ -32,7 +32,7 @@ export interface PaginatedTableProps { columns: Column[]; getRowClassName?: GetRowClassName; rowHeight?: number; - parentContainer?: Element | null; + parentRef?: React.RefObject; initialSortParams?: SortParams; onColumnsResize?: HandleTableColumnsResize; renderControls?: RenderControls; @@ -50,7 +50,7 @@ export const PaginatedTable = ({ columns, getRowClassName, rowHeight = DEFAULT_TABLE_ROW_HEIGHT, - parentContainer, + parentRef, initialSortParams, onColumnsResize, renderControls, @@ -64,10 +64,16 @@ export const PaginatedTable = ({ const [sortParams, setSortParams] = React.useState(initialSortParams); const [totalEntities, setTotalEntities] = React.useState(initialTotal); const [foundEntities, setFoundEntities] = React.useState(initialFound); - const [activeChunks, setActiveChunks] = React.useState([]); const [isInitialLoad, setIsInitialLoad] = React.useState(true); - const tableContainer = React.useRef(null); + const tableRef = React.useRef(null); + + const activeChunks = useScrollBasedChunks({ + containerRef: parentRef ?? tableRef, + totalItems: foundEntities, + itemHeight: rowHeight, + chunkSize: limit, + }); const handleDataFetched = React.useCallback((total: number, found: number) => { setTotalEntities(total); @@ -75,35 +81,19 @@ export const PaginatedTable = ({ setIsInitialLoad(false); }, []); - const onEntry = React.useCallback((id: string) => { - setActiveChunks((prev) => [...new Set([...prev, Number(id)])]); - }, []); - - const onLeave = React.useCallback((id: string) => { - setActiveChunks((prev) => prev.filter((chunk) => chunk !== Number(id))); - }, []); - - const observer = useIntersectionObserver({onEntry, onLeave, parentContainer}); - // reset table on filters change React.useLayoutEffect(() => { setTotalEntities(initialTotal); setFoundEntities(initialFound); setIsInitialLoad(true); - if (parentContainer) { - parentContainer.scrollTo(0, 0); + if (parentRef?.current) { + parentRef.current.scrollTo(0, 0); } else { - tableContainer.current?.scrollTo(0, 0); + tableRef.current?.scrollTo(0, 0); } - - setActiveChunks([0]); - }, [filters, initialFound, initialTotal, limit, parentContainer]); + }, [filters, initialFound, initialTotal, limit, parentRef]); const renderChunks = () => { - if (!observer) { - return null; - } - if (!isInitialLoad && foundEntities === 0) { return ( @@ -133,7 +123,6 @@ export const PaginatedTable = ({ renderErrorMessage={renderErrorMessage} onDataFetched={handleDataFetched} isActive={activeChunks.includes(value)} - observer={observer} /> )); }; @@ -161,7 +150,7 @@ export const PaginatedTable = ({ }; return ( -
+
{renderContent()}
); diff --git a/src/components/PaginatedTable/TableChunk.tsx b/src/components/PaginatedTable/TableChunk.tsx index 430b65b26b..7fd2124b4f 100644 --- a/src/components/PaginatedTable/TableChunk.tsx +++ b/src/components/PaginatedTable/TableChunk.tsx @@ -11,9 +11,6 @@ import type {Column, FetchData, GetRowClassName, SortParams} from './types'; const DEBOUNCE_TIMEOUT = 200; -// With original memo generic types are lost -const typedMemo: (Component: T) => T = React.memo; - interface TableChunkProps { id: number; limit: number; @@ -22,7 +19,6 @@ interface TableChunkProps { columns: Column[]; filters?: F; sortParams?: SortParams; - observer: IntersectionObserver; isActive: boolean; tableName: string; @@ -33,7 +29,7 @@ interface TableChunkProps { } // Memoisation prevents chunks rerenders that could cause perfomance issues on big tables -export const TableChunk = typedMemo(function TableChunk({ +export const TableChunk = ({ id, limit, totalLength, @@ -43,13 +39,11 @@ export const TableChunk = typedMemo(function TableChunk({ tableName, filters, sortParams, - observer, getRowClassName, renderErrorMessage, onDataFetched, isActive, -}: TableChunkProps) { - const ref = React.useRef(null); +}: TableChunkProps) => { const [isTimeoutActive, setIsTimeoutActive] = React.useState(true); const [autoRefreshInterval] = useAutoRefreshInterval(); @@ -83,19 +77,6 @@ export const TableChunk = typedMemo(function TableChunk({ }; }, [isActive, isTimeoutActive]); - React.useEffect(() => { - const el = ref.current; - if (el) { - observer.observe(el); - } - - return () => { - if (el) { - observer.unobserve(el); - } - }; - }, [observer]); - React.useEffect(() => { if (currentData && isActive) { const {total = 0, found = 0} = currentData; @@ -154,7 +135,6 @@ export const TableChunk = typedMemo(function TableChunk({ return ( ({ {renderContent()} ); -}); +}; diff --git a/src/components/PaginatedTable/useIntersectionObserver.ts b/src/components/PaginatedTable/useIntersectionObserver.ts deleted file mode 100644 index af987c1ae8..0000000000 --- a/src/components/PaginatedTable/useIntersectionObserver.ts +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; - -import {DEFAULT_INTERSECTION_OBSERVER_MARGIN} from './constants'; -import type {OnEntry, OnLeave} from './types'; - -interface UseIntersectionObserverProps { - onEntry: OnEntry; - onLeave: OnLeave; - /** Intersection observer calculate margins based on container element properties */ - parentContainer?: Element | null; -} - -export const useIntersectionObserver = ({ - onEntry, - onLeave, - parentContainer, -}: UseIntersectionObserverProps) => { - const observer = React.useRef(); - - React.useEffect(() => { - const callback = (entries: IntersectionObserverEntry[]) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - onEntry(entry.target.id); - } else { - onLeave(entry.target.id); - } - }); - }; - - observer.current = new IntersectionObserver(callback, { - root: parentContainer, - rootMargin: DEFAULT_INTERSECTION_OBSERVER_MARGIN, - }); - - return () => { - observer.current?.disconnect(); - observer.current = undefined; - }; - }, [parentContainer, onEntry, onLeave]); - - return observer.current; -}; diff --git a/src/components/PaginatedTable/useScrollBasedChunks.ts b/src/components/PaginatedTable/useScrollBasedChunks.ts new file mode 100644 index 0000000000..4b96812e36 --- /dev/null +++ b/src/components/PaginatedTable/useScrollBasedChunks.ts @@ -0,0 +1,69 @@ +import React from 'react'; + +import {throttle} from 'lodash'; + +import {getArray} from '../../utils'; + +interface UseScrollBasedChunksProps { + containerRef: React.RefObject; + totalItems: number; + itemHeight: number; + chunkSize: number; +} + +const THROTTLE_DELAY = 100; +const CHUNKS_AHEAD_COUNT = 1; + +export const useScrollBasedChunks = ({ + containerRef, + totalItems, + itemHeight, + chunkSize, +}: UseScrollBasedChunksProps): number[] => { + const [activeChunks, setActiveChunks] = React.useState( + getArray(1 + CHUNKS_AHEAD_COUNT).map((index) => index), + ); + + const calculateActiveChunks = React.useCallback(() => { + const container = containerRef.current; + if (!container) { + return; + } + + const {scrollTop, clientHeight} = container; + const visibleStartIndex = Math.floor(scrollTop / itemHeight); + const visibleEndIndex = Math.min( + Math.ceil((scrollTop + clientHeight) / itemHeight), + 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, + ); + + setActiveChunks(newActiveChunks); + }, [chunkSize, containerRef, itemHeight, totalItems]); + + const throttledCalculateActiveChunks = React.useMemo( + () => throttle(calculateActiveChunks, THROTTLE_DELAY), + [calculateActiveChunks], + ); + + React.useEffect(() => { + const container = containerRef.current; + if (!container) { + return undefined; + } + + container.addEventListener('scroll', throttledCalculateActiveChunks); + return () => { + container.removeEventListener('scroll', throttledCalculateActiveChunks); + throttledCalculateActiveChunks.cancel(); + }; + }, [containerRef, throttledCalculateActiveChunks]); + + return activeChunks; +}; diff --git a/src/containers/Cluster/Cluster.tsx b/src/containers/Cluster/Cluster.tsx index d9f559c4cc..d0fc02dd5b 100644 --- a/src/containers/Cluster/Cluster.tsx +++ b/src/containers/Cluster/Cluster.tsx @@ -164,7 +164,7 @@ export function Cluster({ path={getLocationObjectFromHref(getClusterPath(clusterTabsIds.nodes)).pathname} > @@ -173,7 +173,7 @@ export function Cluster({ getLocationObjectFromHref(getClusterPath(clusterTabsIds.storage)).pathname } > - + - +
); } diff --git a/src/containers/Nodes/NodesWrapper.tsx b/src/containers/Nodes/NodesWrapper.tsx index 4f8fa2dcf3..3cfcd212ce 100644 --- a/src/containers/Nodes/NodesWrapper.tsx +++ b/src/containers/Nodes/NodesWrapper.tsx @@ -8,15 +8,15 @@ import {PaginatedNodes} from './PaginatedNodes'; interface NodesWrapperProps { path?: string; database?: string; - parentContainer?: Element | null; + parentRef?: React.RefObject; additionalNodesProps?: AdditionalNodesProps; } -export const NodesWrapper = ({parentContainer, ...props}: NodesWrapperProps) => { +export const NodesWrapper = ({parentRef, ...props}: NodesWrapperProps) => { const [usePaginatedTables] = useSetting(USE_PAGINATED_TABLES_KEY); if (usePaginatedTables) { - return ; + return ; } return ; diff --git a/src/containers/Nodes/PaginatedNodes.tsx b/src/containers/Nodes/PaginatedNodes.tsx index de3c02c7c5..1055521269 100644 --- a/src/containers/Nodes/PaginatedNodes.tsx +++ b/src/containers/Nodes/PaginatedNodes.tsx @@ -44,16 +44,11 @@ const b = cn('ydb-nodes'); interface NodesProps { path?: string; database?: string; - parentContainer?: Element | null; + parentRef?: React.RefObject; additionalNodesProps?: AdditionalNodesProps; } -export const PaginatedNodes = ({ - path, - database, - parentContainer, - additionalNodesProps, -}: NodesProps) => { +export const PaginatedNodes = ({path, database, parentRef, additionalNodesProps}: NodesProps) => { const [queryParams, setQueryParams] = useQueryParams({ uptimeFilter: StringParam, search: StringParam, @@ -140,7 +135,7 @@ export const PaginatedNodes = ({ return ( ; initialEntitiesCount?: number; } diff --git a/src/containers/Storage/PaginatedStorageGroups.tsx b/src/containers/Storage/PaginatedStorageGroups.tsx index f4b2b0b82e..d3141622f3 100644 --- a/src/containers/Storage/PaginatedStorageGroups.tsx +++ b/src/containers/Storage/PaginatedStorageGroups.tsx @@ -55,7 +55,7 @@ function StorageGroupsComponent({ groupId, pDiskId, viewContext, - parentContainer, + parentRef, initialEntitiesCount, }: PaginatedStorageProps) { const {searchValue, visibleEntities, handleShowAllGroups} = useStorageQueryParams(); @@ -88,7 +88,7 @@ function StorageGroupsComponent({ searchValue={searchValue} visibleEntities={visibleEntities} onShowAll={handleShowAllGroups} - parentContainer={parentContainer} + parentRef={parentRef} renderControls={renderControls} renderErrorMessage={renderPaginatedTableErrorMessage} columns={columnsToShow} diff --git a/src/containers/Storage/PaginatedStorageNodes.tsx b/src/containers/Storage/PaginatedStorageNodes.tsx index e82fb4366f..472cdd54b8 100644 --- a/src/containers/Storage/PaginatedStorageNodes.tsx +++ b/src/containers/Storage/PaginatedStorageNodes.tsx @@ -62,7 +62,7 @@ function StorageNodesComponent({ nodeId, groupId, viewContext, - parentContainer, + parentRef, initialEntitiesCount, }: PaginatedStorageProps) { const {searchValue, visibleEntities, nodesUptimeFilter, handleShowAllNodes} = @@ -96,7 +96,7 @@ function StorageNodesComponent({ visibleEntities={visibleEntities} nodesUptimeFilter={nodesUptimeFilter} onShowAll={handleShowAllNodes} - parentContainer={parentContainer} + parentRef={parentRef} renderControls={renderControls} renderErrorMessage={renderPaginatedTableErrorMessage} columns={columnsToShow} diff --git a/src/containers/Storage/StorageGroups/PaginatedStorageGroupsTable.tsx b/src/containers/Storage/StorageGroups/PaginatedStorageGroupsTable.tsx index 2e526a9010..0630525577 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; - parentContainer?: Element | null; + parentRef?: React.RefObject; renderControls?: RenderControls; renderErrorMessage: RenderErrorMessage; initialEntitiesCount?: number; @@ -49,7 +49,7 @@ export const PaginatedStorageGroupsTable = ({ searchValue, visibleEntities, onShowAll, - parentContainer, + parentRef, renderControls, renderErrorMessage, initialEntitiesCount, @@ -98,7 +98,7 @@ export const PaginatedStorageGroupsTable = ({ ; renderControls?: RenderControls; renderErrorMessage: RenderErrorMessage; initialEntitiesCount?: number; @@ -46,7 +46,7 @@ export const PaginatedStorageNodesTable = ({ visibleEntities, nodesUptimeFilter, onShowAll, - parentContainer, + parentRef, renderControls, renderErrorMessage, initialEntitiesCount, @@ -93,7 +93,7 @@ export const PaginatedStorageNodesTable = ({ return ( ; } -export const StorageWrapper = ({parentContainer, ...props}: StorageWrapperProps) => { +export const StorageWrapper = ({parentRef, ...props}: StorageWrapperProps) => { const [usePaginatedTables] = useSetting(USE_PAGINATED_TABLES_KEY); const viewContext: StorageViewContext = { @@ -25,13 +25,7 @@ export const StorageWrapper = ({parentContainer, ...props}: StorageWrapperProps) }; if (usePaginatedTables) { - return ( - - ); + return ; } return ; diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.tsx b/src/containers/Tenant/Diagnostics/Diagnostics.tsx index 03ada84214..9aefbb38e7 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.tsx +++ b/src/containers/Tenant/Diagnostics/Diagnostics.tsx @@ -46,7 +46,7 @@ interface DiagnosticsProps { const b = cn('kv-tenant-diagnostics'); function Diagnostics(props: DiagnosticsProps) { - const container = React.useRef(null); + const containerRef = React.useRef(null); const dispatch = useTypedDispatch(); const {diagnosticsTab = TENANT_DIAGNOSTICS_TABS_IDS.overview} = useTypedSelector( @@ -106,7 +106,7 @@ function Diagnostics(props: DiagnosticsProps) { path={path} database={tenantName} additionalNodesProps={props.additionalNodesProps} - parentContainer={container.current} + parentRef={containerRef} /> ); } @@ -114,7 +114,7 @@ function Diagnostics(props: DiagnosticsProps) { return ; } case TENANT_DIAGNOSTICS_TABS_IDS.storage: { - return ; + return ; } case TENANT_DIAGNOSTICS_TABS_IDS.network: { return ; @@ -171,14 +171,16 @@ function Diagnostics(props: DiagnosticsProps) { }; return ( -
+
{activeTab ? ( {activeTab.title} ) : null} {renderTabs()} -
{renderTabContent()}
+
+ {renderTabContent()} +
); }