Skip to content

Commit d6197d4

Browse files
feat: add VirtualTable, use for Nodes (#578)
1 parent 0acbfa9 commit d6197d4

28 files changed

+1294
-77
lines changed

src/components/EmptyState/EmptyState.scss

-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
&_size_m {
2020
position: relative;
21-
top: 20%;
2221

2322
width: 800px;
2423
height: 240px;

src/components/ProgressViewer/ProgressViewer.scss

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.progress-viewer {
22
position: relative;
3+
z-index: 0;
34

45
display: flex;
56
overflow: hidden;

src/components/TableWithControlsLayout/TableWithControlsLayout.scss

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
@include sticky-top();
2626
}
2727

28+
.ydb-virtual-table__head {
29+
top: 62px;
30+
}
31+
2832
.data-table__sticky_moving {
2933
// Place table head right after controls
3034
top: 62px !important;
+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import {useEffect, useRef, memo} from 'react';
2+
3+
import type {Column, Chunk, GetRowClassName} from './types';
4+
import {LoadingTableRow, TableRow} from './TableRow';
5+
import {getArray} from './utils';
6+
7+
// With original memo generic types are lost
8+
const typedMemo: <T>(Component: T) => T = memo;
9+
10+
interface TableChunkProps<T> {
11+
id: number;
12+
chunkSize: number;
13+
rowHeight: number;
14+
columns: Column<T>[];
15+
chunkData: Chunk<T> | undefined;
16+
observer: IntersectionObserver;
17+
getRowClassName?: GetRowClassName<T>;
18+
}
19+
20+
// Memoisation prevents chunks rerenders that could cause perfomance issues on big tables
21+
export const TableChunk = typedMemo(function TableChunk<T>({
22+
id,
23+
chunkSize,
24+
rowHeight,
25+
columns,
26+
chunkData,
27+
observer,
28+
getRowClassName,
29+
}: TableChunkProps<T>) {
30+
const ref = useRef<HTMLTableSectionElement>(null);
31+
32+
useEffect(() => {
33+
const el = ref.current;
34+
if (el) {
35+
observer.observe(el);
36+
}
37+
return () => {
38+
if (el) {
39+
observer.unobserve(el);
40+
}
41+
};
42+
}, [observer]);
43+
44+
const dataLength = chunkData?.data?.length;
45+
const chunkHeight = dataLength ? dataLength * rowHeight : chunkSize * rowHeight;
46+
47+
const getLoadingRows = () => {
48+
return getArray(chunkSize).map((value) => {
49+
return (
50+
<LoadingTableRow key={value} columns={columns} height={rowHeight} index={value} />
51+
);
52+
});
53+
};
54+
55+
const renderContent = () => {
56+
if (!chunkData || !chunkData.active) {
57+
return null;
58+
}
59+
60+
// Display skeletons in case of error
61+
if (chunkData.loading || chunkData.error) {
62+
return getLoadingRows();
63+
}
64+
65+
return chunkData.data?.map((data, index) => {
66+
return (
67+
<TableRow
68+
key={index}
69+
index={index}
70+
row={data}
71+
columns={columns}
72+
height={rowHeight}
73+
getRowClassName={getRowClassName}
74+
/>
75+
);
76+
});
77+
};
78+
79+
return (
80+
<tbody ref={ref} id={id.toString()} style={{height: `${chunkHeight}px`}}>
81+
{renderContent()}
82+
</tbody>
83+
);
84+
});
+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import {useState} from 'react';
2+
3+
import type {Column, OnSort, SortOrderType, SortParams} from './types';
4+
import {ASCENDING, DEFAULT_SORT_ORDER, DEFAULT_TABLE_ROW_HEIGHT, DESCENDING} from './constants';
5+
import {b} from './shared';
6+
7+
// Icon similar to original DataTable icons to keep the same tables across diferent pages and tabs
8+
const SortIcon = ({order}: {order?: SortOrderType}) => {
9+
return (
10+
<svg
11+
className={b('icon', {desc: order === DESCENDING})}
12+
viewBox="0 0 10 6"
13+
width="10"
14+
height="6"
15+
>
16+
<path fill="currentColor" d="M0 5h10l-5 -5z" />
17+
</svg>
18+
);
19+
};
20+
21+
interface ColumnSortIconProps {
22+
sortOrder?: SortOrderType;
23+
sortable?: boolean;
24+
defaultSortOrder: SortOrderType;
25+
}
26+
27+
const ColumnSortIcon = ({sortOrder, sortable, defaultSortOrder}: ColumnSortIconProps) => {
28+
if (sortable) {
29+
return (
30+
<span className={b('sort-icon', {shadow: !sortOrder})}>
31+
<SortIcon order={sortOrder || defaultSortOrder} />
32+
</span>
33+
);
34+
} else {
35+
return null;
36+
}
37+
};
38+
39+
interface TableHeadProps<T> {
40+
columns: Column<T>[];
41+
onSort?: OnSort;
42+
defaultSortOrder?: SortOrderType;
43+
rowHeight?: number;
44+
}
45+
46+
export const TableHead = <T,>({
47+
columns,
48+
onSort,
49+
defaultSortOrder = DEFAULT_SORT_ORDER,
50+
rowHeight = DEFAULT_TABLE_ROW_HEIGHT,
51+
}: TableHeadProps<T>) => {
52+
const [sortParams, setSortParams] = useState<SortParams>({});
53+
54+
const handleSort = (columnId: string) => {
55+
let newSortParams: SortParams = {};
56+
57+
// Order is changed in following order:
58+
// 1. Inactive Sort Order - gray icon of default order
59+
// 2. Active default order
60+
// 3. Active not default order
61+
if (columnId === sortParams.columnId) {
62+
if (sortParams.sortOrder && sortParams.sortOrder !== defaultSortOrder) {
63+
setSortParams(newSortParams);
64+
onSort?.(newSortParams);
65+
return;
66+
}
67+
const newSortOrder = sortParams.sortOrder === ASCENDING ? DESCENDING : ASCENDING;
68+
newSortParams = {
69+
sortOrder: newSortOrder,
70+
columnId: columnId,
71+
};
72+
} else {
73+
newSortParams = {
74+
sortOrder: defaultSortOrder,
75+
columnId: columnId,
76+
};
77+
}
78+
79+
onSort?.(newSortParams);
80+
setSortParams(newSortParams);
81+
};
82+
83+
const renderTableColGroups = () => {
84+
return (
85+
<colgroup>
86+
{columns.map((column) => {
87+
return <col key={column.name} style={{width: `${column.width}px`}} />;
88+
})}
89+
</colgroup>
90+
);
91+
};
92+
93+
const renderTableHead = () => {
94+
return (
95+
<thead className={b('head')}>
96+
<tr>
97+
{columns.map((column) => {
98+
const content = column.header ?? column.name;
99+
const sortOrder =
100+
sortParams.columnId === column.name ? sortParams.sortOrder : undefined;
101+
102+
return (
103+
<th
104+
key={column.name}
105+
className={b(
106+
'th',
107+
{align: column.align, sortable: column.sortable},
108+
column.className,
109+
)}
110+
style={{
111+
height: `${rowHeight}px`,
112+
}}
113+
onClick={() => {
114+
handleSort(column.name);
115+
}}
116+
>
117+
<div className={b('head-cell')}>
118+
{content}
119+
<ColumnSortIcon
120+
sortOrder={sortOrder}
121+
sortable={column.sortable}
122+
defaultSortOrder={defaultSortOrder}
123+
/>
124+
</div>
125+
</th>
126+
);
127+
})}
128+
</tr>
129+
</thead>
130+
);
131+
};
132+
133+
return (
134+
<>
135+
{renderTableColGroups()}
136+
{renderTableHead()}
137+
</>
138+
);
139+
};
+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import {type ReactNode} from 'react';
2+
3+
import {Skeleton} from '@gravity-ui/uikit';
4+
5+
import type {AlignType, Column, GetRowClassName} from './types';
6+
import {DEFAULT_ALIGN} from './constants';
7+
import {b} from './shared';
8+
9+
interface TableCellProps {
10+
height: number;
11+
align?: AlignType;
12+
children: ReactNode;
13+
className?: string;
14+
}
15+
16+
const TableRowCell = ({children, className, height, align = DEFAULT_ALIGN}: TableCellProps) => {
17+
return (
18+
<td className={b('td', {align: align}, className)} style={{height: `${height}px`}}>
19+
{children}
20+
</td>
21+
);
22+
};
23+
24+
interface LoadingTableRowProps<T> {
25+
columns: Column<T>[];
26+
index: number;
27+
height: number;
28+
}
29+
30+
export const LoadingTableRow = <T,>({index, columns, height}: LoadingTableRowProps<T>) => {
31+
return (
32+
<tr className={b('row')}>
33+
{columns.map((column) => {
34+
return (
35+
<TableRowCell
36+
key={`${column.name}${index}`}
37+
height={height}
38+
align={column.align}
39+
className={column.className}
40+
>
41+
<Skeleton style={{width: '80%', height: '50%'}} />
42+
</TableRowCell>
43+
);
44+
})}
45+
</tr>
46+
);
47+
};
48+
49+
interface TableRowProps<T> {
50+
columns: Column<T>[];
51+
index: number;
52+
row: T;
53+
height: number;
54+
getRowClassName?: GetRowClassName<T>;
55+
}
56+
57+
export const TableRow = <T,>({row, index, columns, getRowClassName, height}: TableRowProps<T>) => {
58+
const additionalClassName = getRowClassName?.(row);
59+
60+
return (
61+
<tr className={b('row', additionalClassName)}>
62+
{columns.map((column) => {
63+
return (
64+
<TableRowCell
65+
key={`${column.name}${index}`}
66+
height={height}
67+
align={column.align}
68+
className={column.className}
69+
>
70+
{column.render({row, index})}
71+
</TableRowCell>
72+
);
73+
})}
74+
</tr>
75+
);
76+
};
77+
78+
interface EmptyTableRowProps<T> {
79+
columns: Column<T>[];
80+
children?: ReactNode;
81+
}
82+
83+
export const EmptyTableRow = <T,>({columns, children}: EmptyTableRowProps<T>) => {
84+
return (
85+
<tr className={b('row', {empty: true})}>
86+
<td colSpan={columns.length} className={b('td')}>
87+
{children}
88+
</td>
89+
</tr>
90+
);
91+
};

0 commit comments

Comments
 (0)