Skip to content

Commit db4bda6

Browse files
committed
fix: refactoring, true virtualization
1 parent c85b83d commit db4bda6

File tree

5 files changed

+147
-37
lines changed

5 files changed

+147
-37
lines changed

src/components/PaginatedTable/PaginatedTable.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export interface PaginatedTableProps<T, F> {
3838
containerClassName?: string;
3939
}
4040

41-
const DEFAULT_PAGINATION_LIMIT = 20;
41+
const DEFAULT_PAGINATION_LIMIT = 200;
4242

4343
export const PaginatedTable = <T, F>({
4444
limit: chunkSize = DEFAULT_PAGINATION_LIMIT,
@@ -67,7 +67,7 @@ export const PaginatedTable = <T, F>({
6767

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

70-
const activeChunks = useScrollBasedChunks({
70+
const [activeChunks, visibleRange] = useScrollBasedChunks({
7171
parentRef,
7272
tableRef,
7373
totalItems: foundEntities,
@@ -118,6 +118,7 @@ export const PaginatedTable = <T, F>({
118118
renderEmptyDataMessage={renderEmptyDataMessage}
119119
onDataFetched={handleDataFetched}
120120
isActive={isActive}
121+
visibleRange={visibleRange}
121122
/>
122123
));
123124
};

src/components/PaginatedTable/TableChunk.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
RenderErrorMessage,
1717
SortParams,
1818
} from './types';
19+
import type {VisibleRange} from './useScrollBasedChunks';
1920
import {typedMemo} from './utils';
2021

2122
const DEBOUNCE_TIMEOUT = 200;
@@ -30,6 +31,7 @@ interface TableChunkProps<T, F> {
3031
sortParams?: SortParams;
3132
isActive: boolean;
3233
tableName: string;
34+
visibleRange: VisibleRange;
3335

3436
fetchData: FetchData<T, F>;
3537
getRowClassName?: GetRowClassName<T>;
@@ -54,6 +56,7 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
5456
renderEmptyDataMessage,
5557
onDataFetched,
5658
isActive,
59+
visibleRange,
5760
}: TableChunkProps<T, F>) {
5861
const [isTimeoutActive, setIsTimeoutActive] = React.useState(true);
5962
const [autoRefreshInterval] = useAutoRefreshInterval();
@@ -100,6 +103,15 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
100103

101104
const dataLength = currentData?.data?.length || calculatedCount;
102105

106+
// Check if a row is within the visible range
107+
const isRowVisible = (rowIndex: number): boolean => {
108+
// Calculate the absolute row index within the entire table
109+
const absoluteRowIndex = id * chunkSize + rowIndex;
110+
111+
// Check if the row is within the visible range (including overscan)
112+
return absoluteRowIndex >= visibleRange.startRow && absoluteRowIndex <= visibleRange.endRow;
113+
};
114+
103115
const renderContent = () => {
104116
if (!isActive) {
105117
return null;
@@ -119,7 +131,12 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
119131
);
120132
} else {
121133
return getArray(dataLength).map((value) => (
122-
<LoadingTableRow key={value} columns={columns} height={rowHeight} />
134+
<LoadingTableRow
135+
key={value}
136+
columns={columns}
137+
height={rowHeight}
138+
isVisible={isRowVisible(value)}
139+
/>
123140
));
124141
}
125142
}
@@ -140,6 +157,7 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
140157
columns={columns}
141158
height={rowHeight}
142159
getRowClassName={getRowClassName}
160+
isVisible={isRowVisible(index)}
143161
/>
144162
));
145163
};

src/components/PaginatedTable/TableRow.tsx

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,15 @@ interface LoadingTableRowProps<T> {
4242
height: number;
4343
}
4444

45-
export const LoadingTableRow = typedMemo(function <T>({columns, height}: LoadingTableRowProps<T>) {
45+
interface VisibilityProps {
46+
isVisible?: boolean;
47+
}
48+
49+
export const LoadingTableRow = typedMemo(function <T>({
50+
columns,
51+
height,
52+
isVisible = true,
53+
}: LoadingTableRowProps<T> & VisibilityProps) {
4654
return (
4755
<tr className={b('row', {loading: true})}>
4856
{columns.map((column) => {
@@ -57,45 +65,71 @@ export const LoadingTableRow = typedMemo(function <T>({columns, height}: Loading
5765
className={column.className}
5866
resizeable={resizeable}
5967
>
60-
<Skeleton
61-
className={b('row-skeleton')}
62-
style={{width: '80%', height: '50%'}}
63-
/>
68+
{isVisible ? (
69+
<Skeleton
70+
className={b('row-skeleton')}
71+
style={{width: '80%', height: '50%'}}
72+
/>
73+
) : null}
6474
</TableRowCell>
6575
);
6676
})}
6777
</tr>
6878
);
6979
});
7080

81+
interface TableRowColumnProps<T> {
82+
column: Column<T>;
83+
row: T;
84+
height: number;
85+
}
86+
87+
export const TableRowColumn = typedMemo(
88+
<T,>({row, column, height, isVisible = true}: TableRowColumnProps<T> & VisibilityProps) => {
89+
const resizeable = column.resizeable ?? DEFAULT_RESIZEABLE;
90+
91+
return (
92+
<TableRowCell
93+
key={column.name}
94+
height={height}
95+
width={column.width}
96+
align={column.align}
97+
className={column.className}
98+
resizeable={resizeable}
99+
>
100+
{isVisible ? column.render({row}) : null}
101+
</TableRowCell>
102+
);
103+
},
104+
);
105+
71106
interface TableRowProps<T> {
72107
columns: Column<T>[];
73108
row: T;
74109
height: number;
75110
getRowClassName?: GetRowClassName<T>;
76111
}
77112

78-
export const TableRow = <T,>({row, columns, getRowClassName, height}: TableRowProps<T>) => {
113+
export const TableRow = <T,>({
114+
row,
115+
columns,
116+
getRowClassName,
117+
height,
118+
isVisible = true,
119+
}: TableRowProps<T> & VisibilityProps) => {
79120
const additionalClassName = getRowClassName?.(row);
80121

81122
return (
82123
<tr className={b('row', additionalClassName)}>
83-
{columns.map((column) => {
84-
const resizeable = column.resizeable ?? DEFAULT_RESIZEABLE;
85-
86-
return (
87-
<TableRowCell
88-
key={column.name}
89-
height={height}
90-
width={column.width}
91-
align={column.align}
92-
className={column.className}
93-
resizeable={resizeable}
94-
>
95-
{column.render({row})}
96-
</TableRowCell>
97-
);
98-
})}
124+
{columns.map((column) => (
125+
<TableRowColumn
126+
key={column.name}
127+
column={column}
128+
row={row}
129+
height={height}
130+
isVisible={isVisible}
131+
/>
132+
))}
99133
</tr>
100134
);
101135
};

src/components/PaginatedTable/useScrollBasedChunks.ts

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ interface UseScrollBasedChunksProps {
1313
overscanCount?: number;
1414
}
1515

16+
export interface VisibleRange {
17+
startChunk: number;
18+
endChunk: number;
19+
startRow: number;
20+
endRow: number;
21+
}
22+
1623
const DEFAULT_OVERSCAN_COUNT = 1;
1724
const THROTTLE_DELAY = 100;
1825

@@ -23,7 +30,7 @@ export const useScrollBasedChunks = ({
2330
rowHeight,
2431
chunkSize,
2532
overscanCount = DEFAULT_OVERSCAN_COUNT,
26-
}: UseScrollBasedChunksProps): boolean[] => {
33+
}: UseScrollBasedChunksProps): [boolean[], VisibleRange] => {
2734
const chunksCount = React.useMemo(
2835
() => Math.ceil(totalItems / chunkSize),
2936
[chunkSize, totalItems],
@@ -34,6 +41,12 @@ export const useScrollBasedChunks = ({
3441
Math.min(overscanCount, Math.max(chunksCount - 1, 0)),
3542
);
3643

44+
// Track exact visible rows (not just chunks)
45+
const [startRow, setStartRow] = React.useState(0);
46+
const [endRow, setEndRow] = React.useState(
47+
Math.min(overscanCount * chunkSize, Math.max(totalItems - 1, 0)),
48+
);
49+
3750
const calculateVisibleRange = React.useCallback(() => {
3851
const container = parentRef?.current;
3952
const table = tableRef.current;
@@ -46,20 +59,38 @@ export const useScrollBasedChunks = ({
4659
const visibleStart = Math.max(containerScroll - tableOffset, 0);
4760
const visibleEnd = visibleStart + container.clientHeight;
4861

49-
const start = Math.max(Math.floor(visibleStart / rowHeight / chunkSize) - overscanCount, 0);
50-
const end = Math.min(
62+
// Calculate visible chunks (with overscan)
63+
const startChunk = Math.max(
64+
Math.floor(visibleStart / rowHeight / chunkSize) - overscanCount,
65+
0,
66+
);
67+
const endChunk = Math.min(
5168
Math.floor(visibleEnd / rowHeight / chunkSize) + overscanCount,
5269
Math.max(chunksCount - 1, 0),
5370
);
5471

55-
return {start, end};
72+
// Calculate visible rows (more precise)
73+
const startRowIndex = Math.max(Math.floor(visibleStart / rowHeight), 0);
74+
const endRowIndex = Math.min(
75+
Math.floor(visibleEnd / rowHeight),
76+
Math.max(totalItems - 1, 0),
77+
);
78+
79+
return {
80+
start: startChunk,
81+
end: endChunk,
82+
startRow: startRowIndex,
83+
endRow: endRowIndex,
84+
};
5685
}, [parentRef, tableRef, rowHeight, chunkSize, overscanCount, chunksCount]);
5786

5887
const handleScroll = React.useCallback(() => {
5988
const newRange = calculateVisibleRange();
6089
if (newRange) {
6190
setStartChunk(newRange.start);
6291
setEndChunk(newRange.end);
92+
setStartRow(newRange.startRow);
93+
setEndRow(newRange.endRow);
6394
}
6495
}, [calculateVisibleRange]);
6596

@@ -81,12 +112,24 @@ export const useScrollBasedChunks = ({
81112
};
82113
}, [handleScroll, parentRef]);
83114

84-
return React.useMemo(() => {
115+
// Create the visibility information
116+
const activeChunks = React.useMemo(() => {
85117
// boolean array that represents active chunks
86-
const activeChunks = Array(chunksCount).fill(false);
118+
const chunks = Array(chunksCount).fill(false);
87119
for (let i = startChunk; i <= endChunk; i++) {
88-
activeChunks[i] = true;
120+
chunks[i] = true;
89121
}
90-
return activeChunks;
122+
return chunks;
91123
}, [chunksCount, startChunk, endChunk]);
124+
125+
const visibleRange = React.useMemo(() => {
126+
return {
127+
startChunk,
128+
endChunk,
129+
startRow,
130+
endRow,
131+
};
132+
}, [startChunk, endChunk, startRow, endRow]);
133+
134+
return [activeChunks, visibleRange];
92135
};

src/containers/Storage/StorageNodes/getNodes.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const getStorageNodes: FetchData<
4848
type,
4949
storage,
5050
limit,
51-
offset,
51+
offset: 0,
5252
sort,
5353
filter: searchValue,
5454
uptime: getUptimeParamValue(nodesUptimeFilter),
@@ -61,9 +61,23 @@ export const getStorageNodes: FetchData<
6161
fieldsRequired: dataFieldsRequired,
6262
});
6363
const preparedResponse = prepareStorageNodesResponse(response);
64+
65+
let mockedData = preparedResponse.nodes?.slice();
66+
67+
for (let i = 0; i < 4000; i++) {
68+
mockedData = mockedData?.concat(
69+
preparedResponse.nodes?.map((data, j) => ({
70+
...data,
71+
NodeId: data.NodeId + i * 2000 + j,
72+
Host: data.Host || String(i) + ',' + j,
73+
})) || [],
74+
);
75+
}
76+
const paginatedData = mockedData?.slice(offset, offset + limit);
77+
6478
return {
65-
data: preparedResponse.nodes || [],
66-
found: preparedResponse.found || 0,
67-
total: preparedResponse.total || 0,
79+
data: paginatedData || [],
80+
found: mockedData?.length || 0,
81+
total: mockedData?.length || 0,
6882
};
6983
};

0 commit comments

Comments
 (0)