Skip to content

Commit

Permalink
feat: add VirtualTable, use for Nodes (#578)
Browse files Browse the repository at this point in the history
  • Loading branch information
artemmufazalov authored Oct 27, 2023
1 parent 0acbfa9 commit d6197d4
Show file tree
Hide file tree
Showing 28 changed files with 1,294 additions and 77 deletions.
1 change: 0 additions & 1 deletion src/components/EmptyState/EmptyState.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

&_size_m {
position: relative;
top: 20%;

width: 800px;
height: 240px;
Expand Down
1 change: 1 addition & 0 deletions src/components/ProgressViewer/ProgressViewer.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.progress-viewer {
position: relative;
z-index: 0;

display: flex;
overflow: hidden;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
@include sticky-top();
}

.ydb-virtual-table__head {
top: 62px;
}

.data-table__sticky_moving {
// Place table head right after controls
top: 62px !important;
Expand Down
84 changes: 84 additions & 0 deletions src/components/VirtualTable/TableChunk.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {useEffect, useRef, memo} from 'react';

import type {Column, Chunk, GetRowClassName} from './types';
import {LoadingTableRow, TableRow} from './TableRow';
import {getArray} from './utils';

// With original memo generic types are lost
const typedMemo: <T>(Component: T) => T = memo;

interface TableChunkProps<T> {
id: number;
chunkSize: number;
rowHeight: number;
columns: Column<T>[];
chunkData: Chunk<T> | undefined;
observer: IntersectionObserver;
getRowClassName?: GetRowClassName<T>;
}

// Memoisation prevents chunks rerenders that could cause perfomance issues on big tables
export const TableChunk = typedMemo(function TableChunk<T>({
id,
chunkSize,
rowHeight,
columns,
chunkData,
observer,
getRowClassName,
}: TableChunkProps<T>) {
const ref = useRef<HTMLTableSectionElement>(null);

useEffect(() => {
const el = ref.current;
if (el) {
observer.observe(el);
}
return () => {
if (el) {
observer.unobserve(el);
}
};
}, [observer]);

const dataLength = chunkData?.data?.length;
const chunkHeight = dataLength ? dataLength * rowHeight : chunkSize * rowHeight;

const getLoadingRows = () => {
return getArray(chunkSize).map((value) => {
return (
<LoadingTableRow key={value} columns={columns} height={rowHeight} index={value} />
);
});
};

const renderContent = () => {
if (!chunkData || !chunkData.active) {
return null;
}

// Display skeletons in case of error
if (chunkData.loading || chunkData.error) {
return getLoadingRows();
}

return chunkData.data?.map((data, index) => {
return (
<TableRow
key={index}
index={index}
row={data}
columns={columns}
height={rowHeight}
getRowClassName={getRowClassName}
/>
);
});
};

return (
<tbody ref={ref} id={id.toString()} style={{height: `${chunkHeight}px`}}>
{renderContent()}
</tbody>
);
});
139 changes: 139 additions & 0 deletions src/components/VirtualTable/TableHead.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {useState} from 'react';

import type {Column, OnSort, SortOrderType, SortParams} from './types';
import {ASCENDING, DEFAULT_SORT_ORDER, DEFAULT_TABLE_ROW_HEIGHT, DESCENDING} from './constants';
import {b} from './shared';

// Icon similar to original DataTable icons to keep the same tables across diferent pages and tabs
const SortIcon = ({order}: {order?: SortOrderType}) => {
return (
<svg
className={b('icon', {desc: order === DESCENDING})}
viewBox="0 0 10 6"
width="10"
height="6"
>
<path fill="currentColor" d="M0 5h10l-5 -5z" />
</svg>
);
};

interface ColumnSortIconProps {
sortOrder?: SortOrderType;
sortable?: boolean;
defaultSortOrder: SortOrderType;
}

const ColumnSortIcon = ({sortOrder, sortable, defaultSortOrder}: ColumnSortIconProps) => {
if (sortable) {
return (
<span className={b('sort-icon', {shadow: !sortOrder})}>
<SortIcon order={sortOrder || defaultSortOrder} />
</span>
);
} else {
return null;
}
};

interface TableHeadProps<T> {
columns: Column<T>[];
onSort?: OnSort;
defaultSortOrder?: SortOrderType;
rowHeight?: number;
}

export const TableHead = <T,>({
columns,
onSort,
defaultSortOrder = DEFAULT_SORT_ORDER,
rowHeight = DEFAULT_TABLE_ROW_HEIGHT,
}: TableHeadProps<T>) => {
const [sortParams, setSortParams] = useState<SortParams>({});

const handleSort = (columnId: string) => {
let newSortParams: SortParams = {};

// Order is changed in following order:
// 1. Inactive Sort Order - gray icon of default order
// 2. Active default order
// 3. Active not default order
if (columnId === sortParams.columnId) {
if (sortParams.sortOrder && sortParams.sortOrder !== defaultSortOrder) {
setSortParams(newSortParams);
onSort?.(newSortParams);
return;
}
const newSortOrder = sortParams.sortOrder === ASCENDING ? DESCENDING : ASCENDING;
newSortParams = {
sortOrder: newSortOrder,
columnId: columnId,
};
} else {
newSortParams = {
sortOrder: defaultSortOrder,
columnId: columnId,
};
}

onSort?.(newSortParams);
setSortParams(newSortParams);
};

const renderTableColGroups = () => {
return (
<colgroup>
{columns.map((column) => {
return <col key={column.name} style={{width: `${column.width}px`}} />;
})}
</colgroup>
);
};

const renderTableHead = () => {
return (
<thead className={b('head')}>
<tr>
{columns.map((column) => {
const content = column.header ?? column.name;
const sortOrder =
sortParams.columnId === column.name ? sortParams.sortOrder : undefined;

return (
<th
key={column.name}
className={b(
'th',
{align: column.align, sortable: column.sortable},
column.className,
)}
style={{
height: `${rowHeight}px`,
}}
onClick={() => {
handleSort(column.name);
}}
>
<div className={b('head-cell')}>
{content}
<ColumnSortIcon
sortOrder={sortOrder}
sortable={column.sortable}
defaultSortOrder={defaultSortOrder}
/>
</div>
</th>
);
})}
</tr>
</thead>
);
};

return (
<>
{renderTableColGroups()}
{renderTableHead()}
</>
);
};
91 changes: 91 additions & 0 deletions src/components/VirtualTable/TableRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {type ReactNode} from 'react';

import {Skeleton} from '@gravity-ui/uikit';

import type {AlignType, Column, GetRowClassName} from './types';
import {DEFAULT_ALIGN} from './constants';
import {b} from './shared';

interface TableCellProps {
height: number;
align?: AlignType;
children: ReactNode;
className?: string;
}

const TableRowCell = ({children, className, height, align = DEFAULT_ALIGN}: TableCellProps) => {
return (
<td className={b('td', {align: align}, className)} style={{height: `${height}px`}}>
{children}
</td>
);
};

interface LoadingTableRowProps<T> {
columns: Column<T>[];
index: number;
height: number;
}

export const LoadingTableRow = <T,>({index, columns, height}: LoadingTableRowProps<T>) => {
return (
<tr className={b('row')}>
{columns.map((column) => {
return (
<TableRowCell
key={`${column.name}${index}`}
height={height}
align={column.align}
className={column.className}
>
<Skeleton style={{width: '80%', height: '50%'}} />
</TableRowCell>
);
})}
</tr>
);
};

interface TableRowProps<T> {
columns: Column<T>[];
index: number;
row: T;
height: number;
getRowClassName?: GetRowClassName<T>;
}

export const TableRow = <T,>({row, index, columns, getRowClassName, height}: TableRowProps<T>) => {
const additionalClassName = getRowClassName?.(row);

return (
<tr className={b('row', additionalClassName)}>
{columns.map((column) => {
return (
<TableRowCell
key={`${column.name}${index}`}
height={height}
align={column.align}
className={column.className}
>
{column.render({row, index})}
</TableRowCell>
);
})}
</tr>
);
};

interface EmptyTableRowProps<T> {
columns: Column<T>[];
children?: ReactNode;
}

export const EmptyTableRow = <T,>({columns, children}: EmptyTableRowProps<T>) => {
return (
<tr className={b('row', {empty: true})}>
<td colSpan={columns.length} className={b('td')}>
{children}
</td>
</tr>
);
};
Loading

0 comments on commit d6197d4

Please sign in to comment.