ydb-embedded-ui 4.20.4 → 4.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/components/EmptyState/EmptyState.scss +0 -1
- package/dist/components/ProgressViewer/ProgressViewer.scss +1 -0
- package/dist/components/TableWithControlsLayout/TableWithControlsLayout.scss +4 -0
- package/dist/components/VirtualTable/TableChunk.tsx +84 -0
- package/dist/components/VirtualTable/TableHead.tsx +139 -0
- package/dist/components/VirtualTable/TableRow.tsx +91 -0
- package/dist/components/VirtualTable/VirtualTable.scss +146 -0
- package/dist/components/VirtualTable/VirtualTable.tsx +277 -0
- package/dist/components/VirtualTable/constants.ts +17 -0
- package/dist/components/VirtualTable/i18n/en.json +3 -0
- package/dist/components/VirtualTable/i18n/index.ts +11 -0
- package/dist/components/VirtualTable/i18n/ru.json +3 -0
- package/dist/components/VirtualTable/index.ts +3 -0
- package/dist/components/VirtualTable/reducer.ts +143 -0
- package/dist/components/VirtualTable/shared.ts +3 -0
- package/dist/components/VirtualTable/types.ts +60 -0
- package/dist/components/VirtualTable/useIntersectionObserver.ts +42 -0
- package/dist/components/VirtualTable/utils.ts +3 -0
- package/dist/containers/App/App.scss +2 -1
- package/dist/containers/Cluster/Cluster.tsx +17 -4
- package/dist/containers/Nodes/Nodes.tsx +4 -20
- package/dist/containers/Nodes/VirtualNodes.tsx +146 -0
- package/dist/containers/Nodes/getNodes.ts +26 -0
- package/dist/containers/Nodes/getNodesColumns.tsx +49 -39
- package/dist/containers/UserSettings/i18n/en.json +2 -2
- package/dist/containers/UserSettings/i18n/ru.json +2 -2
- package/dist/utils/hooks/useNodesRequestParams.ts +4 -8
- package/dist/utils/nodes.ts +12 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [4.21.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v4.20.4...v4.21.0) (2023-10-27)
|
4
|
+
|
5
|
+
|
6
|
+
### Features
|
7
|
+
|
8
|
+
* add VirtualTable, use for Nodes ([#578](https://github.com/ydb-platform/ydb-embedded-ui/issues/578)) ([d6197d4](https://github.com/ydb-platform/ydb-embedded-ui/commit/d6197d4bebf509596dfff4e1b4a7fe51a847424e))
|
9
|
+
|
3
10
|
## [4.20.4](https://github.com/ydb-platform/ydb-embedded-ui/compare/v4.20.3...v4.20.4) (2023-10-27)
|
4
11
|
|
5
12
|
|
@@ -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
|
+
});
|
@@ -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
|
+
};
|
@@ -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,0 +1,146 @@
|
|
1
|
+
@import '../../styles/mixins.scss';
|
2
|
+
|
3
|
+
.ydb-virtual-table {
|
4
|
+
$block: &;
|
5
|
+
$cell-border: 1px solid var(--virtual-table-border-color);
|
6
|
+
--virtual-table-cell-vertical-padding: 5px;
|
7
|
+
--virtual-table-cell-horizontal-padding: 10px;
|
8
|
+
|
9
|
+
--virtual-table-sort-icon-space: 18px;
|
10
|
+
|
11
|
+
--virtual-table-border-color: var(--yc-color-base-generic-hover);
|
12
|
+
--virtual-table-hover-color: var(--yc-color-base-float-hover);
|
13
|
+
|
14
|
+
width: 100%;
|
15
|
+
@include body2-typography();
|
16
|
+
|
17
|
+
&__table {
|
18
|
+
width: 100%;
|
19
|
+
max-width: 100%;
|
20
|
+
|
21
|
+
table-layout: fixed;
|
22
|
+
border-spacing: 0;
|
23
|
+
border-collapse: separate;
|
24
|
+
}
|
25
|
+
|
26
|
+
&__row {
|
27
|
+
&:hover {
|
28
|
+
background: var(--virtual-table-hover-color);
|
29
|
+
}
|
30
|
+
|
31
|
+
&_empty {
|
32
|
+
&:hover {
|
33
|
+
background-color: initial;
|
34
|
+
}
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
&__head {
|
39
|
+
z-index: 1;
|
40
|
+
@include sticky-top();
|
41
|
+
}
|
42
|
+
|
43
|
+
&__th {
|
44
|
+
position: relative;
|
45
|
+
|
46
|
+
padding: var(--virtual-table-cell-vertical-padding)
|
47
|
+
var(--virtual-table-cell-horizontal-padding);
|
48
|
+
|
49
|
+
font-weight: bold;
|
50
|
+
cursor: default;
|
51
|
+
text-align: left;
|
52
|
+
|
53
|
+
border-bottom: $cell-border;
|
54
|
+
|
55
|
+
&_sortable {
|
56
|
+
cursor: pointer;
|
57
|
+
|
58
|
+
#{$block}__head-cell {
|
59
|
+
padding-right: var(--virtual-table-sort-icon-space);
|
60
|
+
}
|
61
|
+
|
62
|
+
&#{$block}__th_align_right {
|
63
|
+
#{$block}__head-cell {
|
64
|
+
padding-right: 0;
|
65
|
+
padding-left: var(--virtual-table-sort-icon-space);
|
66
|
+
}
|
67
|
+
|
68
|
+
#{$block}__sort-icon {
|
69
|
+
right: auto;
|
70
|
+
left: 0;
|
71
|
+
|
72
|
+
transform: translate(0, -50%) scaleX(-1);
|
73
|
+
}
|
74
|
+
}
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
&__head-cell {
|
79
|
+
position: relative;
|
80
|
+
|
81
|
+
display: inline-block;
|
82
|
+
overflow: hidden;
|
83
|
+
|
84
|
+
box-sizing: border-box;
|
85
|
+
max-width: 100%;
|
86
|
+
|
87
|
+
vertical-align: top;
|
88
|
+
white-space: nowrap;
|
89
|
+
text-overflow: ellipsis;
|
90
|
+
}
|
91
|
+
|
92
|
+
&__sort-icon {
|
93
|
+
position: absolute;
|
94
|
+
top: 50%;
|
95
|
+
right: 0;
|
96
|
+
|
97
|
+
display: inline-flex;
|
98
|
+
|
99
|
+
color: inherit;
|
100
|
+
|
101
|
+
transform: translate(0, -50%);
|
102
|
+
|
103
|
+
&_shadow {
|
104
|
+
opacity: 0.15;
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
&__icon {
|
109
|
+
vertical-align: top;
|
110
|
+
|
111
|
+
&_desc {
|
112
|
+
transform: rotate(180deg);
|
113
|
+
}
|
114
|
+
}
|
115
|
+
|
116
|
+
&__td {
|
117
|
+
overflow: hidden;
|
118
|
+
|
119
|
+
padding: var(--virtual-table-cell-vertical-padding)
|
120
|
+
var(--virtual-table-cell-horizontal-padding);
|
121
|
+
|
122
|
+
white-space: nowrap;
|
123
|
+
text-overflow: ellipsis;
|
124
|
+
|
125
|
+
border-bottom: $cell-border;
|
126
|
+
}
|
127
|
+
|
128
|
+
&__td,
|
129
|
+
&__th {
|
130
|
+
height: 40px;
|
131
|
+
|
132
|
+
vertical-align: middle;
|
133
|
+
|
134
|
+
&_align {
|
135
|
+
&_left {
|
136
|
+
text-align: left;
|
137
|
+
}
|
138
|
+
&_center {
|
139
|
+
text-align: center;
|
140
|
+
}
|
141
|
+
&_right {
|
142
|
+
text-align: right;
|
143
|
+
}
|
144
|
+
}
|
145
|
+
}
|
146
|
+
}
|