Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 16 additions & 21 deletions src/components/PaginatedTable/PaginatedTable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';

import {getArray} from '../../utils';
import {TableWithControlsLayout} from '../TableWithControlsLayout/TableWithControlsLayout';

import {TableChunk} from './TableChunk';
Expand Down Expand Up @@ -32,7 +31,7 @@ export interface PaginatedTableProps<T, F> {
columns: Column<T>[];
getRowClassName?: GetRowClassName<T>;
rowHeight?: number;
parentRef?: React.RefObject<HTMLElement>;
parentRef: React.RefObject<HTMLElement>;
initialSortParams?: SortParams;
onColumnsResize?: HandleTableColumnsResize;
renderControls?: RenderControls;
Expand All @@ -42,7 +41,7 @@ export interface PaginatedTableProps<T, F> {
}

export const PaginatedTable = <T, F>({
limit,
limit: chunkSize,
initialEntitiesCount,
fetchData,
filters,
Expand All @@ -58,8 +57,8 @@ export const PaginatedTable = <T, F>({
renderEmptyDataMessage,
containerClassName,
}: PaginatedTableProps<T, F>) => {
const initialTotal = initialEntitiesCount || limit;
const initialFound = initialEntitiesCount || 0;
const initialTotal = initialEntitiesCount || 0;
const initialFound = initialEntitiesCount || 1;

const [sortParams, setSortParams] = React.useState<SortParams | undefined>(initialSortParams);
const [totalEntities, setTotalEntities] = React.useState(initialTotal);
Expand All @@ -68,11 +67,12 @@ export const PaginatedTable = <T, F>({

const tableRef = React.useRef<HTMLDivElement>(null);

const activeChunks = useScrollBasedChunks({
containerRef: parentRef ?? tableRef,
const chunks = useScrollBasedChunks({
parentRef,
tableRef,
totalItems: foundEntities,
itemHeight: rowHeight,
chunkSize: limit,
rowHeight,
chunkSize,
});

const handleDataFetched = React.useCallback((total: number, found: number) => {
Expand All @@ -88,10 +88,8 @@ export const PaginatedTable = <T, F>({
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) {
Expand All @@ -104,15 +102,12 @@ export const PaginatedTable = <T, F>({
);
}

const totalLength = foundEntities || limit;
const chunksCount = Math.ceil(totalLength / limit);

return getArray(chunksCount).map((value) => (
return chunks.map((itemsCount, index) => (
<TableChunk<T, F>
key={value}
id={value}
limit={limit}
totalLength={totalLength}
key={index}
id={index}
itemsCount={itemsCount}
chunkSize={chunkSize}
rowHeight={rowHeight}
columns={columns}
fetchData={fetchData}
Expand All @@ -122,7 +117,7 @@ export const PaginatedTable = <T, F>({
getRowClassName={getRowClassName}
renderErrorMessage={renderErrorMessage}
onDataFetched={handleDataFetched}
isActive={activeChunks.includes(value)}
isActive={Boolean(itemsCount)}
/>
));
};
Expand Down
29 changes: 12 additions & 17 deletions src/components/PaginatedTable/TableChunk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ 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;

interface TableChunkProps<T, F> {
id: number;
limit: number;
totalLength: number;
chunkSize: number;
rowHeight: number;
itemsCount: number;
columns: Column<T>[];
filters?: F;
sortParams?: SortParams;
Expand All @@ -29,10 +30,10 @@ interface TableChunkProps<T, F> {
}

// Memoisation prevents chunks rerenders that could cause perfomance issues on big tables
export const TableChunk = <T, F>({
export const TableChunk = typedMemo(function TableChunk<T, F>({
id,
limit,
totalLength,
chunkSize,
itemsCount,
rowHeight,
columns,
fetchData,
Expand All @@ -43,15 +44,15 @@ export const TableChunk = <T, F>({
renderErrorMessage,
onDataFetched,
isActive,
}: TableChunkProps<T, F>) => {
}: TableChunkProps<T, F>) {
const [isTimeoutActive, setIsTimeoutActive] = React.useState(true);
const [autoRefreshInterval] = useAutoRefreshInterval();

const columnsIds = columns.map((column) => column.name);

const queryParams = {
offset: id * limit,
limit,
offset: id * chunkSize,
limit: chunkSize,
fetchData: fetchData as FetchData<T, unknown>,
filters,
sortParams,
Expand Down Expand Up @@ -87,11 +88,7 @@ export const TableChunk = <T, F>({
}
}, [currentData, isActive, onDataFetched]);

const chunkOffset = id * limit;
const remainingLength = totalLength - chunkOffset;
const calculatedChunkLength = remainingLength < limit ? remainingLength : limit;

const dataLength = currentData?.data?.length || calculatedChunkLength;
const dataLength = currentData?.data?.length || itemsCount || chunkSize;

const renderContent = () => {
if (!isActive) {
Expand Down Expand Up @@ -134,13 +131,11 @@ export const TableChunk = <T, F>({
));
};

const chunkHeight = dataLength ? dataLength * rowHeight : limit * rowHeight;

return (
<tbody
id={id.toString()}
style={{
height: `${chunkHeight}px`,
height: `${dataLength * rowHeight}px`,
// Default display: table-row-group doesn't work in Safari and breaks the table
// display: block works in Safari, but disconnects thead and tbody cell grids
// Hack to make it work in all cases
Expand All @@ -150,4 +145,4 @@ export const TableChunk = <T, F>({
{renderContent()}
</tbody>
);
};
});
2 changes: 1 addition & 1 deletion src/components/PaginatedTable/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ export const DEFAULT_SORT_ORDER = DESCENDING;
// Time in ms after which request will be sent
export const DEFAULT_REQUEST_TIMEOUT = 200;

export const DEFAULT_TABLE_ROW_HEIGHT = 40;
export const DEFAULT_TABLE_ROW_HEIGHT = 41;

export const DEFAULT_INTERSECTION_OBSERVER_MARGIN = '100%';
97 changes: 62 additions & 35 deletions src/components/PaginatedTable/useScrollBasedChunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,95 @@ import React from 'react';

import {throttle} from 'lodash';

import {getArray} from '../../utils';
import {calculateElementOffsetTop} from './utils';

interface UseScrollBasedChunksProps {
containerRef: React.RefObject<HTMLElement>;
parentRef: React.RefObject<HTMLElement>;
tableRef: React.RefObject<HTMLElement>;
totalItems: number;
itemHeight: number;
rowHeight: number;
chunkSize: number;
overscanCount?: number;
}

const DEFAULT_OVERSCAN_COUNT = 1;
const THROTTLE_DELAY = 100;
const CHUNKS_AHEAD_COUNT = 1;

export const useScrollBasedChunks = ({
containerRef,
parentRef,
tableRef,
totalItems,
itemHeight,
rowHeight,
chunkSize,
overscanCount = DEFAULT_OVERSCAN_COUNT,
}: UseScrollBasedChunksProps): number[] => {
const [activeChunks, setActiveChunks] = React.useState<number[]>(
getArray(1 + CHUNKS_AHEAD_COUNT).map((index) => index),
const chunksCount = React.useMemo(
() => Math.ceil(totalItems / chunkSize),
[chunkSize, totalItems],
);

const calculateActiveChunks = React.useCallback(() => {
const container = containerRef.current;
if (!container) {
return;
}
const [startChunk, setStartChunk] = React.useState(0);
const [endChunk, setEndChunk] = React.useState(
Math.min(overscanCount, Math.max(chunksCount - 1, 0)),
);

const {scrollTop, clientHeight} = container;
const visibleStartIndex = Math.floor(scrollTop / itemHeight);
const visibleEndIndex = Math.min(
Math.ceil((scrollTop + clientHeight) / itemHeight),
totalItems - 1,
);
const calculateVisibleRange = React.useCallback(() => {
const container = parentRef?.current;
const table = tableRef.current;
if (!container || !table) {
return null;
}

const startChunk = Math.floor(visibleStartIndex / chunkSize);
const endChunk = Math.floor(visibleEndIndex / chunkSize);
const tableOffset = calculateElementOffsetTop(table, container);
const containerScroll = container.scrollTop;
const visibleStart = Math.max(containerScroll - tableOffset, 0);
const visibleEnd = visibleStart + container.clientHeight;

const newActiveChunks = getArray(endChunk - startChunk + 1 + CHUNKS_AHEAD_COUNT).map(
(index) => startChunk + index,
const start = Math.max(Math.floor(visibleStart / rowHeight / chunkSize) - overscanCount, 0);
const end = Math.min(
Math.floor(visibleEnd / rowHeight / chunkSize) + overscanCount,
Math.max(chunksCount - 1, 0),
);

setActiveChunks(newActiveChunks);
}, [chunkSize, containerRef, itemHeight, totalItems]);
return {start, end};
}, [parentRef, tableRef, rowHeight, chunkSize, overscanCount, chunksCount]);

const throttledCalculateActiveChunks = React.useMemo(
() => throttle(calculateActiveChunks, THROTTLE_DELAY),
[calculateActiveChunks],
);
const handleScroll = React.useCallback(() => {
const newRange = calculateVisibleRange();
if (newRange) {
setStartChunk(newRange.start);
setEndChunk(newRange.end);
}
}, [calculateVisibleRange]);

React.useEffect(() => {
const container = containerRef.current;
const container = parentRef?.current;
if (!container) {
return undefined;
}

container.addEventListener('scroll', throttledCalculateActiveChunks);
const throttledHandleScroll = throttle(handleScroll, THROTTLE_DELAY, {
leading: true,
trailing: true,
});

container.addEventListener('scroll', throttledHandleScroll);
return () => {
container.removeEventListener('scroll', throttledCalculateActiveChunks);
throttledCalculateActiveChunks.cancel();
container.removeEventListener('scroll', throttledHandleScroll);
throttledHandleScroll.cancel();
};
}, [containerRef, throttledCalculateActiveChunks]);
}, [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;
}

const lastChunkSize = totalItems % chunkSize || chunkSize;
chunks[endChunk] = endChunk === chunksCount - 1 ? lastChunkSize : chunkSize;

return activeChunks;
return chunks;
}, [chunksCount, startChunk, endChunk, totalItems, chunkSize]);
};
Original file line number Diff line number Diff line change
@@ -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<Fn extends (...args: any[]) => any>(fn: Fn) {
Expand All @@ -23,3 +25,17 @@ export function rafThrottle<Fn extends (...args: any[]) => any>(fn: Fn) {
export function calculateColumnWidth(newWidth: number, minWidth = 40, maxWidth = Infinity) {
return Math.max(minWidth, Math.min(newWidth, maxWidth));
}

export const typedMemo: <T>(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;
}
4 changes: 2 additions & 2 deletions src/containers/Node/Node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export function Node(props: NodeProps) {
switch (activeTabVerified.id) {
case STORAGE: {
return (
<div className={b('storage')}>
<div className={b('storage')} ref={container}>
<StorageWrapper nodeId={nodeId} parentRef={container} />
</div>
);
Expand Down Expand Up @@ -159,7 +159,7 @@ export function Node(props: NodeProps) {

if (node) {
return (
<div className={b(null, props.className)} ref={container}>
<div className={b(null, props.className)}>
<Helmet
titleTemplate={`%s — ${node.Host} — YDB Monitoring`}
defaultTitle={`${node.Host} — YDB Monitoring`}
Expand Down
2 changes: 1 addition & 1 deletion src/containers/Nodes/NodesWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {PaginatedNodes} from './PaginatedNodes';
interface NodesWrapperProps {
path?: string;
database?: string;
parentRef?: React.RefObject<HTMLElement>;
parentRef: React.RefObject<HTMLElement>;
additionalNodesProps?: AdditionalNodesProps;
}

Expand Down
2 changes: 1 addition & 1 deletion src/containers/Nodes/PaginatedNodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const b = cn('ydb-nodes');
interface NodesProps {
path?: string;
database?: string;
parentRef?: React.RefObject<HTMLElement>;
parentRef: React.RefObject<HTMLElement>;
additionalNodesProps?: AdditionalNodesProps;
}

Expand Down
Loading
Loading