ydb-embedded-ui 4.20.4 → 4.21.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/components/EmptyState/EmptyState.scss +0 -1
  3. package/dist/components/ProgressViewer/ProgressViewer.scss +1 -0
  4. package/dist/components/TableWithControlsLayout/TableWithControlsLayout.scss +4 -0
  5. package/dist/components/VirtualTable/TableChunk.tsx +84 -0
  6. package/dist/components/VirtualTable/TableHead.tsx +139 -0
  7. package/dist/components/VirtualTable/TableRow.tsx +91 -0
  8. package/dist/components/VirtualTable/VirtualTable.scss +146 -0
  9. package/dist/components/VirtualTable/VirtualTable.tsx +277 -0
  10. package/dist/components/VirtualTable/constants.ts +17 -0
  11. package/dist/components/VirtualTable/i18n/en.json +3 -0
  12. package/dist/components/VirtualTable/i18n/index.ts +11 -0
  13. package/dist/components/VirtualTable/i18n/ru.json +3 -0
  14. package/dist/components/VirtualTable/index.ts +3 -0
  15. package/dist/components/VirtualTable/reducer.ts +143 -0
  16. package/dist/components/VirtualTable/shared.ts +3 -0
  17. package/dist/components/VirtualTable/types.ts +60 -0
  18. package/dist/components/VirtualTable/useIntersectionObserver.ts +42 -0
  19. package/dist/components/VirtualTable/utils.ts +3 -0
  20. package/dist/containers/App/App.scss +2 -1
  21. package/dist/containers/Cluster/Cluster.tsx +17 -4
  22. package/dist/containers/Nodes/Nodes.tsx +4 -20
  23. package/dist/containers/Nodes/VirtualNodes.tsx +146 -0
  24. package/dist/containers/Nodes/getNodes.ts +26 -0
  25. package/dist/containers/Nodes/getNodesColumns.tsx +49 -39
  26. package/dist/containers/UserSettings/i18n/en.json +2 -2
  27. package/dist/containers/UserSettings/i18n/ru.json +2 -2
  28. package/dist/utils/hooks/useNodesRequestParams.ts +4 -8
  29. package/dist/utils/nodes.ts +12 -0
  30. 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
 
@@ -18,7 +18,6 @@
18
18
 
19
19
  &_size_m {
20
20
  position: relative;
21
- top: 20%;
22
21
 
23
22
  width: 800px;
24
23
  height: 240px;
@@ -1,5 +1,6 @@
1
1
  .progress-viewer {
2
2
  position: relative;
3
+ z-index: 0;
3
4
 
4
5
  display: flex;
5
6
  overflow: hidden;
@@ -25,6 +25,10 @@
25
25
  @include sticky-top();
26
26
  }
27
27
 
28
+ .ydb-virtual-table__head {
29
+ top: 62px;
30
+ }
31
+
28
32
  .data-table__sticky_moving {
29
33
  // Place table head right after controls
30
34
  top: 62px !important;
@@ -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
+ }