ydb-embedded-ui 4.32.0 → 4.33.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. package/dist/components/CellWithPopover/CellWithPopover.scss +7 -0
  2. package/dist/components/Loader/Loader.tsx +3 -2
  3. package/dist/components/MetricChart/MetricChart.tsx +45 -4
  4. package/dist/components/MetricChart/index.ts +1 -1
  5. package/dist/components/MetricChart/types.ts +3 -0
  6. package/dist/components/VirtualTable/TableHead.tsx +127 -26
  7. package/dist/components/VirtualTable/TableRow.tsx +15 -2
  8. package/dist/components/VirtualTable/VirtualTable.scss +62 -60
  9. package/dist/components/VirtualTable/VirtualTable.tsx +11 -1
  10. package/dist/components/VirtualTable/types.ts +1 -0
  11. package/dist/containers/Nodes/VirtualNodes.tsx +5 -1
  12. package/dist/containers/Nodes/getNodesColumns.tsx +1 -0
  13. package/dist/containers/Tenant/Diagnostics/Overview/TableInfo/prepareTableInfo.ts +2 -2
  14. package/dist/containers/Tenant/Diagnostics/TenantOverview/DefaultOverviewContent/DefaultOverviewContent.tsx +6 -0
  15. package/dist/containers/Tenant/Diagnostics/TenantOverview/{DefaultDashboard.tsx → DefaultOverviewContent/defaultDashboardConfig.ts} +3 -7
  16. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.tsx +3 -2
  17. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/{CpuDashboard.tsx → cpuDashboardConfig.ts} +2 -6
  18. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.tsx +27 -9
  19. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TenantMemory.tsx +3 -2
  20. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/{MemoryDashboard.tsx → memoryDashboardConfig.ts} +2 -6
  21. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx +3 -5
  22. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantOverviewTableLayout.tsx +1 -3
  23. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.tsx +6 -6
  24. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/{StorageDashboard.tsx → storageDashboardConfig.ts} +2 -6
  25. package/dist/containers/Tenant/Diagnostics/TenantOverview/utils.ts +3 -0
  26. package/dist/containers/UserSettings/i18n/en.json +1 -4
  27. package/dist/containers/UserSettings/i18n/ru.json +1 -4
  28. package/dist/containers/UserSettings/settings.ts +1 -12
  29. package/dist/services/settings.ts +0 -2
  30. package/dist/utils/constants.ts +0 -2
  31. package/dist/utils/hooks/useTableResize.ts +53 -0
  32. package/package.json +1 -1
@@ -1,13 +1,20 @@
1
1
  .ydb-cell-with-popover {
2
2
  display: flex;
3
3
 
4
+ max-width: 100%;
5
+
4
6
  &__popover {
5
7
  display: inline-block;
6
8
  overflow: hidden;
7
9
 
8
10
  max-width: 100%;
9
11
 
12
+ vertical-align: middle;
10
13
  white-space: nowrap;
11
14
  text-overflow: ellipsis;
15
+
16
+ .yc-popover__handler {
17
+ display: inline;
18
+ }
12
19
  }
13
20
  }
@@ -7,11 +7,12 @@ const b = cn('ydb-loader');
7
7
 
8
8
  interface LoaderProps {
9
9
  size?: LoaderSize;
10
+ className?: string;
10
11
  }
11
12
 
12
- export const Loader = ({size = 'm'}: LoaderProps) => {
13
+ export const Loader = ({size = 'm', className}: LoaderProps) => {
13
14
  return (
14
- <div className={b()}>
15
+ <div className={b(null, className)}>
15
16
  <KitLoader size={size} />
16
17
  </div>
17
18
  );
@@ -6,13 +6,19 @@ import ChartKit, {settings} from '@gravity-ui/chartkit';
6
6
  import type {IResponseError} from '../../types/api/error';
7
7
  import type {TimeFrame} from '../../utils/timeframes';
8
8
  import {useAutofetcher} from '../../utils/hooks';
9
+
9
10
  import {COLORS} from '../../utils/versions';
10
11
  import {cn} from '../../utils/cn';
11
12
 
12
13
  import {Loader} from '../Loader';
13
14
  import {ResponseError} from '../Errors/ResponseError';
14
15
 
15
- import type {ChartOptions, MetricDescription, PreparedMetricsData} from './types';
16
+ import type {
17
+ ChartOptions,
18
+ MetricDescription,
19
+ OnChartDataStatusChange,
20
+ PreparedMetricsData,
21
+ } from './types';
16
22
  import {convertResponse} from './convertReponse';
17
23
  import {getDefaultDataFormatter} from './getDefaultDataFormatter';
18
24
  import {getChartData} from './getChartData';
@@ -102,6 +108,15 @@ interface DiagnosticsChartProps {
102
108
  width?: number;
103
109
 
104
110
  chartOptions?: ChartOptions;
111
+
112
+ onChartDataStatusChange?: OnChartDataStatusChange;
113
+
114
+ /**
115
+ * YAGR charts don't render correctly inside not visible elements\
116
+ * So if chart is used inside component with 'display:none', it will be empty, when visibility change\
117
+ * Pass isChartVisible prop to ensure proper chart render
118
+ */
119
+ isChartVisible?: boolean;
105
120
  }
106
121
 
107
122
  export const MetricChart = ({
@@ -112,6 +127,8 @@ export const MetricChart = ({
112
127
  width = 400,
113
128
  height = width / 1.5,
114
129
  chartOptions,
130
+ onChartDataStatusChange,
131
+ isChartVisible,
115
132
  }: DiagnosticsChartProps) => {
116
133
  const mounted = useRef(false);
117
134
 
@@ -127,6 +144,20 @@ export const MetricChart = ({
127
144
  initialChartState,
128
145
  );
129
146
 
147
+ useEffect(() => {
148
+ if (error) {
149
+ return onChartDataStatusChange?.('error');
150
+ }
151
+ if (loading && !wasLoaded) {
152
+ return onChartDataStatusChange?.('loading');
153
+ }
154
+ if (!loading && wasLoaded) {
155
+ return onChartDataStatusChange?.('success');
156
+ }
157
+
158
+ return undefined;
159
+ }, [loading, wasLoaded, error, onChartDataStatusChange]);
160
+
130
161
  const fetchChartData = useCallback(
131
162
  async (isBackground: boolean) => {
132
163
  dispatch(setChartDataLoading());
@@ -146,7 +177,9 @@ export const MetricChart = ({
146
177
  });
147
178
 
148
179
  // Hack to prevent setting value to state, if component unmounted
149
- if (!mounted.current) return;
180
+ if (!mounted.current) {
181
+ return;
182
+ }
150
183
 
151
184
  // In some cases error could be in response with 200 status code
152
185
  // It happens when request is OK, but chart data cannot be returned due to some reason
@@ -155,10 +188,14 @@ export const MetricChart = ({
155
188
  const preparedData = convertResponse(response, metrics);
156
189
  dispatch(setChartData(preparedData));
157
190
  } else {
158
- dispatch(setChartError({statusText: response.error}));
191
+ const err = {statusText: response.error};
192
+
193
+ throw err;
159
194
  }
160
195
  } catch (err) {
161
- if (!mounted.current) return;
196
+ if (!mounted.current) {
197
+ return;
198
+ }
162
199
 
163
200
  dispatch(setChartError(err as IResponseError));
164
201
  }
@@ -175,6 +212,10 @@ export const MetricChart = ({
175
212
  return <Loader />;
176
213
  }
177
214
 
215
+ if (!isChartVisible) {
216
+ return null;
217
+ }
218
+
178
219
  return (
179
220
  <div className={b('chart')}>
180
221
  <ChartKit type="yagr" data={convertedData} />
@@ -1,2 +1,2 @@
1
- export type {MetricDescription, Metric, ChartOptions} from './types';
1
+ export * from './types';
2
2
  export {MetricChart} from './MetricChart';
@@ -30,3 +30,6 @@ export type ChartDataType = 'ms' | 'size';
30
30
  export interface ChartOptions {
31
31
  dataType?: ChartDataType;
32
32
  }
33
+
34
+ export type ChartDataStatus = 'loading' | 'success' | 'error';
35
+ export type OnChartDataStatusChange = (newStatus: ChartDataStatus) => void;
@@ -1,14 +1,21 @@
1
- import {useState} from 'react';
1
+ import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
2
+
3
+ import type {
4
+ HandleTableColumnsResize,
5
+ TableColumnsWidthSetup,
6
+ } from '../../utils/hooks/useTableResize';
2
7
 
3
8
  import type {Column, OnSort, SortOrderType, SortParams} from './types';
4
9
  import {ASCENDING, DEFAULT_SORT_ORDER, DEFAULT_TABLE_ROW_HEIGHT, DESCENDING} from './constants';
5
10
  import {b} from './shared';
6
11
 
12
+ const COLUMN_NAME_HTML_ATTRIBUTE = 'data-columnname';
13
+
7
14
  // Icon similar to original DataTable icons to keep the same tables across diferent pages and tabs
8
15
  const SortIcon = ({order}: {order?: SortOrderType}) => {
9
16
  return (
10
17
  <svg
11
- className={b('icon', {desc: order === DESCENDING})}
18
+ className={b('sort-icon', {desc: order === DESCENDING})}
12
19
  viewBox="0 0 10 6"
13
20
  width="10"
14
21
  height="6"
@@ -27,7 +34,7 @@ interface ColumnSortIconProps {
27
34
  const ColumnSortIcon = ({sortOrder, sortable, defaultSortOrder}: ColumnSortIconProps) => {
28
35
  if (sortable) {
29
36
  return (
30
- <span className={b('sort-icon', {shadow: !sortOrder})}>
37
+ <span className={b('sort-icon-container', {shadow: !sortOrder})}>
31
38
  <SortIcon order={sortOrder || defaultSortOrder} />
32
39
  </span>
33
40
  );
@@ -36,9 +43,84 @@ const ColumnSortIcon = ({sortOrder, sortable, defaultSortOrder}: ColumnSortIconP
36
43
  }
37
44
  };
38
45
 
46
+ interface TableHeadCellProps<T> {
47
+ column: Column<T>;
48
+ sortOrder?: SortOrderType;
49
+ defaultSortOrder: SortOrderType;
50
+ onSort?: (columnName: string) => void;
51
+ rowHeight: number;
52
+ onCellMount?: (element: Element) => void;
53
+ onCellUnMount?: (element: Element) => void;
54
+ }
55
+
56
+ export const TableHeadCell = <T,>({
57
+ column,
58
+ sortOrder,
59
+ defaultSortOrder,
60
+ onSort,
61
+ rowHeight,
62
+ onCellMount,
63
+ onCellUnMount,
64
+ }: TableHeadCellProps<T>) => {
65
+ const cellWrapperRef = useRef<HTMLDivElement>(null);
66
+
67
+ useEffect(() => {
68
+ const cellWrapper = cellWrapperRef.current;
69
+ if (cellWrapper) {
70
+ onCellMount?.(cellWrapper);
71
+ }
72
+ return () => {
73
+ if (cellWrapper) {
74
+ onCellUnMount?.(cellWrapper);
75
+ }
76
+ };
77
+ }, [onCellMount, onCellUnMount]);
78
+
79
+ const content = column.header ?? column.name;
80
+
81
+ return (
82
+ <th>
83
+ <div
84
+ ref={cellWrapperRef}
85
+ className={b('head-cell-wrapper', {
86
+ resizeable: column.resizeable,
87
+ })}
88
+ style={{
89
+ height: `${rowHeight}px`,
90
+ width: `${column.width}px`,
91
+ }}
92
+ {...{
93
+ [COLUMN_NAME_HTML_ATTRIBUTE]: column.name,
94
+ }}
95
+ >
96
+ <div
97
+ className={b(
98
+ 'head-cell',
99
+ {align: column.align, sortable: column.sortable},
100
+ column.className,
101
+ )}
102
+ onClick={() => {
103
+ if (column.sortable) {
104
+ onSort?.(column.name);
105
+ }
106
+ }}
107
+ >
108
+ <div className={b('head-cell-content')}>{content}</div>
109
+ <ColumnSortIcon
110
+ sortOrder={sortOrder}
111
+ sortable={column.sortable}
112
+ defaultSortOrder={defaultSortOrder}
113
+ />
114
+ </div>
115
+ </div>
116
+ </th>
117
+ );
118
+ };
119
+
39
120
  interface TableHeadProps<T> {
40
121
  columns: Column<T>[];
41
122
  onSort?: OnSort;
123
+ onColumnsResize?: HandleTableColumnsResize;
42
124
  defaultSortOrder?: SortOrderType;
43
125
  rowHeight?: number;
44
126
  }
@@ -46,11 +128,44 @@ interface TableHeadProps<T> {
46
128
  export const TableHead = <T,>({
47
129
  columns,
48
130
  onSort,
131
+ onColumnsResize,
49
132
  defaultSortOrder = DEFAULT_SORT_ORDER,
50
133
  rowHeight = DEFAULT_TABLE_ROW_HEIGHT,
51
134
  }: TableHeadProps<T>) => {
52
135
  const [sortParams, setSortParams] = useState<SortParams>({});
53
136
 
137
+ const isTableResizeable = Boolean(onColumnsResize);
138
+
139
+ const resizeObserver: ResizeObserver | undefined = useMemo(() => {
140
+ if (!isTableResizeable) {
141
+ return undefined;
142
+ }
143
+
144
+ return new ResizeObserver((entries) => {
145
+ const columnsWidth: TableColumnsWidthSetup = {};
146
+ entries.forEach((entry) => {
147
+ // @ts-ignore ignore custrom property usage
148
+ const id = entry.target.attributes[COLUMN_NAME_HTML_ATTRIBUTE]?.value;
149
+ columnsWidth[id] = entry.contentRect.width;
150
+ });
151
+
152
+ onColumnsResize?.(columnsWidth);
153
+ });
154
+ }, [onColumnsResize, isTableResizeable]);
155
+
156
+ const handleCellMount = useCallback(
157
+ (element: Element) => {
158
+ resizeObserver?.observe(element);
159
+ },
160
+ [resizeObserver],
161
+ );
162
+ const handleCellUnMount = useCallback(
163
+ (element: Element) => {
164
+ resizeObserver?.unobserve(element);
165
+ },
166
+ [resizeObserver],
167
+ );
168
+
54
169
  const handleSort = (columnId: string) => {
55
170
  let newSortParams: SortParams = {};
56
171
 
@@ -95,34 +210,20 @@ export const TableHead = <T,>({
95
210
  <thead className={b('head')}>
96
211
  <tr>
97
212
  {columns.map((column) => {
98
- const content = column.header ?? column.name;
99
213
  const sortOrder =
100
214
  sortParams.columnId === column.name ? sortParams.sortOrder : undefined;
101
215
 
102
216
  return (
103
- <th
217
+ <TableHeadCell
104
218
  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>
219
+ column={column}
220
+ sortOrder={sortOrder}
221
+ defaultSortOrder={defaultSortOrder}
222
+ onSort={handleSort}
223
+ rowHeight={rowHeight}
224
+ onCellMount={handleCellMount}
225
+ onCellUnMount={handleCellUnMount}
226
+ />
126
227
  );
127
228
  })}
128
229
  </tr>
@@ -8,14 +8,25 @@ import {b} from './shared';
8
8
 
9
9
  interface TableCellProps {
10
10
  height: number;
11
+ width: number;
11
12
  align?: AlignType;
12
13
  children: ReactNode;
13
14
  className?: string;
14
15
  }
15
16
 
16
- const TableRowCell = ({children, className, height, align = DEFAULT_ALIGN}: TableCellProps) => {
17
+ const TableRowCell = ({
18
+ children,
19
+ className,
20
+ height,
21
+ width,
22
+ align = DEFAULT_ALIGN,
23
+ }: TableCellProps) => {
24
+ // Additional maxWidth to ensure overflow hidden for <td>
17
25
  return (
18
- <td className={b('td', {align: align}, className)} style={{height: `${height}px`}}>
26
+ <td
27
+ className={b('row-cell', {align: align}, className)}
28
+ style={{height: `${height}px`, width: `${width}px`, maxWidth: `${width}px`}}
29
+ >
19
30
  {children}
20
31
  </td>
21
32
  );
@@ -35,6 +46,7 @@ export const LoadingTableRow = <T,>({index, columns, height}: LoadingTableRowPro
35
46
  <TableRowCell
36
47
  key={`${column.name}${index}`}
37
48
  height={height}
49
+ width={column.width}
38
50
  align={column.align}
39
51
  className={column.className}
40
52
  >
@@ -64,6 +76,7 @@ export const TableRow = <T,>({row, index, columns, getRowClassName, height}: Tab
64
76
  <TableRowCell
65
77
  key={`${column.name}${index}`}
66
78
  height={height}
79
+ width={column.width}
67
80
  align={column.align}
68
81
  className={column.className}
69
82
  >
@@ -6,8 +6,6 @@
6
6
  --virtual-table-cell-vertical-padding: 5px;
7
7
  --virtual-table-cell-horizontal-padding: 10px;
8
8
 
9
- --virtual-table-sort-icon-space: 18px;
10
-
11
9
  --virtual-table-border-color: var(--g-color-base-generic-hover);
12
10
  --virtual-table-hover-color: var(--g-color-base-float-hover);
13
11
 
@@ -21,6 +19,10 @@
21
19
  table-layout: fixed;
22
20
  border-spacing: 0;
23
21
  border-collapse: separate;
22
+
23
+ th {
24
+ padding: 0;
25
+ }
24
26
  }
25
27
 
26
28
  &__row {
@@ -40,96 +42,96 @@
40
42
  @include sticky-top();
41
43
  }
42
44
 
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;
45
+ &__sort-icon-container {
46
+ display: flex;
47
+ justify-content: center;
52
48
 
53
- border-bottom: $cell-border;
49
+ color: inherit;
54
50
 
55
- &_sortable {
56
- cursor: pointer;
51
+ &_shadow {
52
+ opacity: 0.15;
53
+ }
54
+ }
57
55
 
58
- #{$block}__head-cell {
59
- padding-right: var(--virtual-table-sort-icon-space);
60
- }
56
+ &__sort-icon {
57
+ &_desc {
58
+ transform: rotate(180deg);
59
+ }
60
+ }
61
61
 
62
- &#{$block}__th_align_right {
63
- #{$block}__head-cell {
64
- padding-right: 0;
65
- padding-left: var(--virtual-table-sort-icon-space);
66
- }
62
+ &__head-cell-wrapper {
63
+ display: flex;
64
+ overflow-x: hidden;
67
65
 
68
- #{$block}__sort-icon {
69
- right: auto;
70
- left: 0;
66
+ border-bottom: $cell-border;
71
67
 
72
- transform: translate(0, -50%) scaleX(-1);
73
- }
74
- }
68
+ &_resizeable {
69
+ resize: horizontal;
75
70
  }
76
71
  }
77
72
 
78
73
  &__head-cell {
79
- position: relative;
80
-
81
- display: inline-block;
82
- overflow: hidden;
74
+ display: flex;
75
+ flex-direction: row;
76
+ align-items: center;
83
77
 
84
- box-sizing: border-box;
78
+ width: 100%;
85
79
  max-width: 100%;
80
+ padding: var(--virtual-table-cell-vertical-padding)
81
+ var(--virtual-table-cell-horizontal-padding);
86
82
 
87
- vertical-align: top;
88
- white-space: nowrap;
89
- text-overflow: ellipsis;
83
+ &_align {
84
+ &_left {
85
+ justify-content: left;
86
+ }
87
+ &_center {
88
+ justify-content: center;
89
+ }
90
+ &_right {
91
+ justify-content: right;
92
+ }
93
+ }
90
94
  }
91
95
 
92
- &__sort-icon {
93
- position: absolute;
94
- top: 50%;
95
- right: 0;
96
-
97
- display: inline-flex;
96
+ &__head-cell {
97
+ gap: 8px;
98
98
 
99
- color: inherit;
99
+ font-weight: bold;
100
+ cursor: default;
100
101
 
101
- transform: translate(0, -50%);
102
+ &_sortable {
103
+ cursor: pointer;
102
104
 
103
- &_shadow {
104
- opacity: 0.15;
105
+ &#{$block}__head-cell_align_right {
106
+ flex-direction: row-reverse;
107
+ }
105
108
  }
106
109
  }
107
110
 
108
- &__icon {
109
- vertical-align: top;
111
+ // Separate head cell content class for correct text ellipsis overflow
112
+ &__head-cell-content {
113
+ overflow: hidden;
110
114
 
111
- &_desc {
112
- transform: rotate(180deg);
113
- }
115
+ width: min-content;
116
+
117
+ white-space: nowrap;
118
+ text-overflow: ellipsis;
114
119
  }
115
120
 
116
- &__td {
117
- overflow: hidden;
121
+ &__row-cell {
122
+ display: table-cell;
123
+ overflow-x: hidden;
118
124
 
125
+ width: 100%;
126
+ max-width: 100%;
119
127
  padding: var(--virtual-table-cell-vertical-padding)
120
128
  var(--virtual-table-cell-horizontal-padding);
121
129
 
130
+ vertical-align: middle;
122
131
  white-space: nowrap;
123
132
  text-overflow: ellipsis;
124
133
 
125
134
  border-bottom: $cell-border;
126
- }
127
-
128
- &__td,
129
- &__th {
130
- height: 40px;
131
-
132
- vertical-align: middle;
133
135
 
134
136
  &_align {
135
137
  &_left {
@@ -1,5 +1,7 @@
1
1
  import {useState, useReducer, useRef, useCallback, useEffect} from 'react';
2
2
 
3
+ import type {HandleTableColumnsResize} from '../../utils/hooks/useTableResize';
4
+
3
5
  import type {IResponseError} from '../../types/api/error';
4
6
  import {getArray} from '../../utils';
5
7
 
@@ -45,9 +47,12 @@ interface VirtualTableProps<T> {
45
47
  rowHeight?: number;
46
48
  parentContainer?: Element | null;
47
49
  initialSortParams?: SortParams;
50
+ onColumnsResize?: HandleTableColumnsResize;
51
+
48
52
  renderControls?: RenderControls;
49
53
  renderEmptyDataMessage?: RenderEmptyDataMessage;
50
54
  renderErrorMessage?: RenderErrorMessage;
55
+
51
56
  dependencyArray?: unknown[]; // Fully reload table on params change
52
57
  }
53
58
 
@@ -59,6 +64,7 @@ export const VirtualTable = <T,>({
59
64
  rowHeight = DEFAULT_TABLE_ROW_HEIGHT,
60
65
  parentContainer,
61
66
  initialSortParams,
67
+ onColumnsResize,
62
68
  renderControls,
63
69
  renderEmptyDataMessage,
64
70
  renderErrorMessage,
@@ -258,7 +264,11 @@ export const VirtualTable = <T,>({
258
264
  const renderTable = () => {
259
265
  return (
260
266
  <table className={b('table')}>
261
- <TableHead columns={columns} onSort={handleSort} />
267
+ <TableHead
268
+ columns={columns}
269
+ onSort={handleSort}
270
+ onColumnsResize={onColumnsResize}
271
+ />
262
272
  {renderData()}
263
273
  </table>
264
274
  );
@@ -28,6 +28,7 @@ export interface Column<T> {
28
28
  header?: ReactNode;
29
29
  className?: string;
30
30
  sortable?: boolean;
31
+ resizeable?: boolean;
31
32
  render: (props: {row: T; index: number}) => ReactNode;
32
33
  width: number;
33
34
  align: AlignType;
@@ -13,6 +13,7 @@ import {
13
13
  isSortableNodesProperty,
14
14
  isUnavailableNode,
15
15
  } from '../../utils/nodes';
16
+ import {updateColumnsWidth, useTableResize} from '../../utils/hooks/useTableResize';
16
17
 
17
18
  import {Search} from '../../components/Search';
18
19
  import {ProblemFilter} from '../../components/ProblemFilter';
@@ -50,6 +51,8 @@ export const VirtualNodes = ({path, parentContainer, additionalNodesProps}: Node
50
51
  NodesUptimeFilterValues.All,
51
52
  );
52
53
 
54
+ const [tableColumnsWidthSetup, setTableColumnsWidth] = useTableResize('nodesTableColumnsWidth');
55
+
53
56
  const filters = useMemo(() => {
54
57
  return [path, searchValue, problemFilter, uptimeFilter];
55
58
  }, [path, searchValue, problemFilter, uptimeFilter]);
@@ -118,7 +121,7 @@ export const VirtualNodes = ({path, parentContainer, additionalNodesProps}: Node
118
121
  getNodeRef: additionalNodesProps?.getNodeRef,
119
122
  });
120
123
 
121
- const columns = rawColumns.map((column) => {
124
+ const columns = updateColumnsWidth(rawColumns, tableColumnsWidthSetup).map((column) => {
122
125
  return {...column, sortable: isSortableNodesProperty(column.name)};
123
126
  });
124
127
 
@@ -133,6 +136,7 @@ export const VirtualNodes = ({path, parentContainer, additionalNodesProps}: Node
133
136
  renderEmptyDataMessage={renderEmptyDataMessage}
134
137
  dependencyArray={filters}
135
138
  getRowClassName={getRowClassName}
139
+ onColumnsResize={setTableColumnsWidth}
136
140
  />
137
141
  );
138
142
  };
@@ -89,6 +89,7 @@ const versionColumn: NodesColumn = {
89
89
  return <CellWithPopover content={row.Version}>{row.Version}</CellWithPopover>;
90
90
  },
91
91
  sortable: false,
92
+ resizeable: true,
92
93
  };
93
94
 
94
95
  const uptimeColumn: NodesColumn = {
@@ -100,14 +100,14 @@ const prepareTableGeneralInfo = (PartitionConfig: TPartitionConfig, TTLSettings?
100
100
  {label: 'Partitioning by load', value: partitioningByLoad},
101
101
  {
102
102
  label: 'Min number of partitions',
103
- value: PartitioningPolicy.MinPartitionsCount || 0,
103
+ value: formatNumber(PartitioningPolicy.MinPartitionsCount || 0),
104
104
  },
105
105
  );
106
106
 
107
107
  if (PartitioningPolicy.MaxPartitionsCount) {
108
108
  generalTableInfo.push({
109
109
  label: 'Max number of partitions',
110
- value: PartitioningPolicy.MaxPartitionsCount || 0,
110
+ value: formatNumber(PartitioningPolicy.MaxPartitionsCount),
111
111
  });
112
112
  }
113
113
 
@@ -0,0 +1,6 @@
1
+ import {TenantDashboard} from '../TenantDashboard/TenantDashboard';
2
+ import {defaultDashboardConfig} from './defaultDashboardConfig';
3
+
4
+ export const DefaultOverviewContent = () => {
5
+ return <TenantDashboard charts={defaultDashboardConfig} />;
6
+ };
@@ -1,7 +1,7 @@
1
- import {type ChartConfig, TenantDashboard} from './TenantDashboard/TenantDashboard';
2
- import i18n from './i18n';
1
+ import type {ChartConfig} from '../TenantDashboard/TenantDashboard';
2
+ import i18n from '../i18n';
3
3
 
4
- const defaultDashboardConfig: ChartConfig[] = [
4
+ export const defaultDashboardConfig: ChartConfig[] = [
5
5
  {
6
6
  title: i18n('charts.queries-per-second'),
7
7
  metrics: [
@@ -44,7 +44,3 @@ const defaultDashboardConfig: ChartConfig[] = [
44
44
  },
45
45
  },
46
46
  ];
47
-
48
- export const DefaultDashboard = () => {
49
- return <TenantDashboard charts={defaultDashboardConfig} />;
50
- };
@@ -1,5 +1,6 @@
1
1
  import type {AdditionalNodesProps} from '../../../../../types/additionalProps';
2
- import {CpuDashboard} from './CpuDashboard';
2
+ import {TenantDashboard} from '../TenantDashboard/TenantDashboard';
3
+ import {cpuDashboardConfig} from './cpuDashboardConfig';
3
4
  import {TopNodesByLoad} from './TopNodesByLoad';
4
5
  import {TopNodesByCpu} from './TopNodesByCpu';
5
6
  import {TopShards} from './TopShards';
@@ -13,7 +14,7 @@ interface TenantCpuProps {
13
14
  export function TenantCpu({path, additionalNodesProps}: TenantCpuProps) {
14
15
  return (
15
16
  <>
16
- <CpuDashboard />
17
+ <TenantDashboard charts={cpuDashboardConfig} />
17
18
  <TopNodesByLoad path={path} additionalNodesProps={additionalNodesProps} />
18
19
  <TopNodesByCpu path={path} additionalNodesProps={additionalNodesProps} />
19
20
  <TopShards path={path} />
@@ -1,7 +1,7 @@
1
- import {type ChartConfig, TenantDashboard} from '../TenantDashboard/TenantDashboard';
1
+ import type {ChartConfig} from '../TenantDashboard/TenantDashboard';
2
2
  import i18n from '../i18n';
3
3
 
4
- const cpuDashboardConfig: ChartConfig[] = [
4
+ export const cpuDashboardConfig: ChartConfig[] = [
5
5
  {
6
6
  title: i18n('charts.cpu-usage'),
7
7
  metrics: [
@@ -12,7 +12,3 @@ const cpuDashboardConfig: ChartConfig[] = [
12
12
  ],
13
13
  },
14
14
  ];
15
-
16
- export const CpuDashboard = () => {
17
- return <TenantDashboard charts={cpuDashboardConfig} />;
18
- };
@@ -1,14 +1,15 @@
1
+ import {useState} from 'react';
1
2
  import {StringParam, useQueryParam} from 'use-query-params';
2
3
 
3
4
  import {cn} from '../../../../../utils/cn';
4
5
  import type {TimeFrame} from '../../../../../utils/timeframes';
5
- import {useSetting, useTypedSelector} from '../../../../../utils/hooks';
6
- import {DISPLAY_CHARTS_IN_DB_DIAGNOSTICS_KEY} from '../../../../../utils/constants';
6
+ import {useTypedSelector} from '../../../../../utils/hooks';
7
7
  import {TimeFrameSelector} from '../../../../../components/TimeFrameSelector/TimeFrameSelector';
8
8
  import {
9
9
  type ChartOptions,
10
10
  MetricChart,
11
11
  type MetricDescription,
12
+ type ChartDataStatus,
12
13
  } from '../../../../../components/MetricChart';
13
14
 
14
15
  import './TenantDashboard.scss';
@@ -29,15 +30,29 @@ interface TenantDashboardProps {
29
30
  }
30
31
 
31
32
  export const TenantDashboard = ({charts}: TenantDashboardProps) => {
33
+ const [isDashboardHidden, setIsDashboardHidden] = useState<boolean>(true);
34
+
32
35
  const [timeFrame = '1h', setTimeframe] = useQueryParam('timeframe', StringParam);
33
36
 
34
37
  const {autorefresh} = useTypedSelector((state) => state.schema);
35
38
 
36
- const [chartsEnabled] = useSetting(DISPLAY_CHARTS_IN_DB_DIAGNOSTICS_KEY);
39
+ // Refetch data only if dashboard successfully loaded
40
+ const shouldRefresh = autorefresh && !isDashboardHidden;
37
41
 
38
- if (!chartsEnabled) {
39
- return null;
40
- }
42
+ /**
43
+ * Charts should be hidden, if they are not enabled:
44
+ * 1. GraphShard is not enabled
45
+ * 2. ydb version does not have /viewer/json/render endpoint (400, 404, CORS error, etc.)
46
+ *
47
+ * If at least one chart successfully loaded, dashboard should be shown
48
+ * @link https://github.com/ydb-platform/ydb-embedded-ui/issues/659
49
+ * @todo disable only for specific errors ('GraphShard is not enabled') after ydb-stable-24 is generally used
50
+ */
51
+ const handleChartDataStatusChange = (chartStatus: ChartDataStatus) => {
52
+ if (chartStatus === 'success') {
53
+ setIsDashboardHidden(false);
54
+ }
55
+ };
41
56
 
42
57
  // If there is only one chart, display it with full width
43
58
  const chartWidth = charts.length === 1 ? CHART_WIDTH_FULL : CHART_WIDTH;
@@ -45,23 +60,26 @@ export const TenantDashboard = ({charts}: TenantDashboardProps) => {
45
60
 
46
61
  const renderContent = () => {
47
62
  return charts.map((chartConfig) => {
63
+ const chartId = chartConfig.metrics.map(({target}) => target).join('&');
48
64
  return (
49
65
  <MetricChart
50
- key={chartConfig.metrics.map(({target}) => target).join('&')}
66
+ key={chartId}
51
67
  title={chartConfig.title}
52
68
  metrics={chartConfig.metrics}
53
69
  timeFrame={timeFrame as TimeFrame}
54
70
  chartOptions={chartConfig.options}
55
- autorefresh={autorefresh}
71
+ autorefresh={shouldRefresh}
56
72
  width={chartWidth}
57
73
  height={chartHeight}
74
+ onChartDataStatusChange={handleChartDataStatusChange}
75
+ isChartVisible={!isDashboardHidden}
58
76
  />
59
77
  );
60
78
  });
61
79
  };
62
80
 
63
81
  return (
64
- <div className={b(null)}>
82
+ <div className={b(null)} style={{display: isDashboardHidden ? 'none' : undefined}}>
65
83
  <div className={b('controls')}>
66
84
  <TimeFrameSelector value={timeFrame as TimeFrame} onChange={setTimeframe} />
67
85
  </div>
@@ -1,4 +1,5 @@
1
- import {MemoryDashboard} from './MemoryDashboard';
1
+ import {TenantDashboard} from '../TenantDashboard/TenantDashboard';
2
+ import {memoryDashboardConfig} from './memoryDashboardConfig';
2
3
  import {TopNodesByMemory} from './TopNodesByMemory';
3
4
 
4
5
  interface TenantMemoryProps {
@@ -8,7 +9,7 @@ interface TenantMemoryProps {
8
9
  export function TenantMemory({path}: TenantMemoryProps) {
9
10
  return (
10
11
  <>
11
- <MemoryDashboard />
12
+ <TenantDashboard charts={memoryDashboardConfig} />
12
13
  <TopNodesByMemory path={path} />
13
14
  </>
14
15
  );
@@ -1,7 +1,7 @@
1
- import {type ChartConfig, TenantDashboard} from '../TenantDashboard/TenantDashboard';
1
+ import type {ChartConfig} from '../TenantDashboard/TenantDashboard';
2
2
  import i18n from '../i18n';
3
3
 
4
- const memoryDashboardConfig: ChartConfig[] = [
4
+ export const memoryDashboardConfig: ChartConfig[] = [
5
5
  {
6
6
  title: i18n('charts.memory-usage'),
7
7
  metrics: [
@@ -15,7 +15,3 @@ const memoryDashboardConfig: ChartConfig[] = [
15
15
  },
16
16
  },
17
17
  ];
18
-
19
- export const MemoryDashboard = () => {
20
- return <TenantDashboard charts={memoryDashboardConfig} />;
21
- };
@@ -1,4 +1,3 @@
1
- import cn from 'bem-cn-lite';
2
1
  import {useCallback} from 'react';
3
2
  import {useDispatch} from 'react-redux';
4
3
 
@@ -17,12 +16,11 @@ import {HealthcheckDetails} from './Healthcheck/HealthcheckDetails';
17
16
  import {MetricsCards, type TenantMetrics} from './MetricsCards/MetricsCards';
18
17
  import {TenantStorage} from './TenantStorage/TenantStorage';
19
18
  import {TenantMemory} from './TenantMemory/TenantMemory';
20
- import {DefaultDashboard} from './DefaultDashboard';
19
+ import {DefaultOverviewContent} from './DefaultOverviewContent/DefaultOverviewContent';
21
20
  import {useHealthcheck} from './useHealthcheck';
22
21
 
23
22
  import './TenantOverview.scss';
24
-
25
- const b = cn('tenant-overview');
23
+ import {b} from './utils';
26
24
 
27
25
  interface TenantOverviewProps {
28
26
  tenantName: string;
@@ -141,7 +139,7 @@ export function TenantOverview({
141
139
  );
142
140
  }
143
141
  default: {
144
- return <DefaultDashboard />;
142
+ return <DefaultOverviewContent />;
145
143
  }
146
144
  }
147
145
  };
@@ -1,5 +1,4 @@
1
1
  import type {ReactNode} from 'react';
2
- import cn from 'bem-cn-lite';
3
2
 
4
3
  import DataTable from '@gravity-ui/react-data-table';
5
4
  import type {DataTableProps} from '@gravity-ui/react-data-table';
@@ -11,8 +10,7 @@ import {
11
10
  import type {IResponseError} from '../../../../types/api/error';
12
11
  import {TableSkeleton} from '../../../../components/TableSkeleton/TableSkeleton';
13
12
  import {ResponseError} from '../../../../components/Errors/ResponseError';
14
-
15
- const b = cn('tenant-overview');
13
+ import {b} from './utils';
16
14
 
17
15
  interface TenantOverviewTableLayoutProps<T> extends Omit<DataTableProps<T>, 'theme'> {
18
16
  title: ReactNode;
@@ -1,17 +1,16 @@
1
- import cn from 'bem-cn-lite';
2
-
3
1
  import InfoViewer from '../../../../../components/InfoViewer/InfoViewer';
4
2
  import {ProgressViewer} from '../../../../../components/ProgressViewer/ProgressViewer';
5
3
  import {formatStorageValues} from '../../../../../utils/dataFormatters/dataFormatters';
6
4
  import {getSizeWithSignificantDigits} from '../../../../../utils/bytesParsers';
7
5
 
6
+ import {TenantDashboard} from '../TenantDashboard/TenantDashboard';
7
+
8
8
  import '../TenantOverview.scss';
9
9
 
10
- import {StorageDashboard} from './StorageDashboard';
10
+ import {storageDashboardConfig} from './storageDashboardConfig';
11
11
  import {TopTables} from './TopTables';
12
12
  import {TopGroups} from './TopGroups';
13
-
14
- const b = cn('tenant-overview');
13
+ import {b} from '../utils';
15
14
 
16
15
  export interface TenantStorageMetrics {
17
16
  blobStorageUsed?: number;
@@ -61,9 +60,10 @@ export function TenantStorage({tenantName, metrics}: TenantStorageProps) {
61
60
  ),
62
61
  },
63
62
  ];
63
+
64
64
  return (
65
65
  <>
66
- <StorageDashboard />
66
+ <TenantDashboard charts={storageDashboardConfig} />
67
67
  <InfoViewer className={b('storage-info')} title="Storage details" info={info} />
68
68
  <TopTables path={tenantName} />
69
69
  <TopGroups tenant={tenantName} />
@@ -1,7 +1,7 @@
1
- import {type ChartConfig, TenantDashboard} from '../TenantDashboard/TenantDashboard';
1
+ import type {ChartConfig} from '../TenantDashboard/TenantDashboard';
2
2
  import i18n from '../i18n';
3
3
 
4
- const storageDashboardConfig: ChartConfig[] = [
4
+ export const storageDashboardConfig: ChartConfig[] = [
5
5
  {
6
6
  title: i18n('charts.storage-usage'),
7
7
  metrics: [
@@ -15,7 +15,3 @@ const storageDashboardConfig: ChartConfig[] = [
15
15
  },
16
16
  },
17
17
  ];
18
-
19
- export const StorageDashboard = () => {
20
- return <TenantDashboard charts={storageDashboardConfig} />;
21
- };
@@ -0,0 +1,3 @@
1
+ import {cn} from '../../../../utils/cn';
2
+
3
+ export const b = cn('tenant-overview');
@@ -23,8 +23,5 @@
23
23
  "settings.useVirtualTables.popover": "It will increase performance, but could work unstable",
24
24
 
25
25
  "settings.queryUseMultiSchema.title": "Allow queries with multiple result sets",
26
- "settings.queryUseMultiSchema.popover": "Use 'multi' schema for queries that enables queries with multiple result sets. Returns nothing on versions 23-3 and older",
27
-
28
- "settings.displayChartsInDbDiagnostics.title": "Display charts in database diagnostics",
29
- "settings.displayChartsInDbDiagnostics.popover": "Incorrect data may be displayed (shows data only for the database related to the current node), does not work well on static nodes"
26
+ "settings.queryUseMultiSchema.popover": "Use 'multi' schema for queries that enables queries with multiple result sets. Returns nothing on versions 23-3 and older"
30
27
  }
@@ -23,8 +23,5 @@
23
23
  "settings.useVirtualTables.popover": "Это улучшит производительность, но может работать нестабильно",
24
24
 
25
25
  "settings.queryUseMultiSchema.title": "Разрешить запросы с несколькими результатами",
26
- "settings.queryUseMultiSchema.popover": "Использовать для запросов схему 'multi', которая позволяет выполнять запросы с несколькими результатами. На версиях 23-3 и старше результат не возвращается вообще",
27
-
28
- "settings.displayChartsInDbDiagnostics.title": "Показывать графики в диагностике базы данных",
29
- "settings.displayChartsInDbDiagnostics.popover": "Могут отображаться неправильные данные (показывает данные только для базы, относящейся к текущей ноде), плохо работает на статических нодах"
26
+ "settings.queryUseMultiSchema.popover": "Использовать для запросов схему 'multi', которая позволяет выполнять запросы с несколькими результатами. На версиях 23-3 и старше результат не возвращается вообще"
30
27
  }
@@ -10,7 +10,6 @@ import {
10
10
  USE_BACKEND_PARAMS_FOR_TABLES_KEY,
11
11
  USE_NODES_ENDPOINT_IN_DIAGNOSTICS_KEY,
12
12
  QUERY_USE_MULTI_SCHEMA_KEY,
13
- DISPLAY_CHARTS_IN_DB_DIAGNOSTICS_KEY,
14
13
  } from '../../utils/constants';
15
14
  import {Lang, defaultLang} from '../../utils/i18n';
16
15
 
@@ -95,11 +94,6 @@ export const queryUseMultiSchemaSetting: SettingProps = {
95
94
  title: i18n('settings.queryUseMultiSchema.title'),
96
95
  helpPopoverContent: i18n('settings.queryUseMultiSchema.popover'),
97
96
  };
98
- export const displayChartsInDbDiagnosticsSetting: SettingProps = {
99
- settingKey: DISPLAY_CHARTS_IN_DB_DIAGNOSTICS_KEY,
100
- title: i18n('settings.displayChartsInDbDiagnostics.title'),
101
- helpPopoverContent: i18n('settings.displayChartsInDbDiagnostics.popover'),
102
- };
103
97
 
104
98
  export const appearanceSection: SettingsSection = {
105
99
  id: 'appearanceSection',
@@ -109,12 +103,7 @@ export const appearanceSection: SettingsSection = {
109
103
  export const experimentsSection: SettingsSection = {
110
104
  id: 'experimentsSection',
111
105
  title: i18n('section.experiments'),
112
- settings: [
113
- useNodesEndpointSetting,
114
- useVirtualTables,
115
- queryUseMultiSchemaSetting,
116
- displayChartsInDbDiagnosticsSetting,
117
- ],
106
+ settings: [useNodesEndpointSetting, useVirtualTables, queryUseMultiSchemaSetting],
118
107
  };
119
108
 
120
109
  export const generalPage: SettingsPage = {
@@ -3,7 +3,6 @@ import {TENANT_PAGES_IDS} from '../store/reducers/tenant/constants';
3
3
  import {
4
4
  ASIDE_HEADER_COMPACT_KEY,
5
5
  CLUSTER_INFO_HIDDEN_KEY,
6
- DISPLAY_CHARTS_IN_DB_DIAGNOSTICS_KEY,
7
6
  INVERTED_DISKS_KEY,
8
7
  LANGUAGE_KEY,
9
8
  LAST_USED_QUERY_ACTION_KEY,
@@ -38,7 +37,6 @@ export const DEFAULT_USER_SETTINGS: SettingsObject = {
38
37
  [PARTITIONS_HIDDEN_COLUMNS_KEY]: [],
39
38
  [CLUSTER_INFO_HIDDEN_KEY]: true,
40
39
  [USE_BACKEND_PARAMS_FOR_TABLES_KEY]: false,
41
- [DISPLAY_CHARTS_IN_DB_DIAGNOSTICS_KEY]: false,
42
40
  };
43
41
 
44
42
  class SettingsManager {
@@ -127,5 +127,3 @@ export const USE_BACKEND_PARAMS_FOR_TABLES_KEY = 'useBackendParamsForTables';
127
127
 
128
128
  // Enable schema that supports multiple resultsets
129
129
  export const QUERY_USE_MULTI_SCHEMA_KEY = 'queryUseMultiSchema';
130
-
131
- export const DISPLAY_CHARTS_IN_DB_DIAGNOSTICS_KEY = 'displayChartsInDbDiagnostics';
@@ -0,0 +1,53 @@
1
+ import {useCallback, useState} from 'react';
2
+ import type {Column as DataTableColumn} from '@gravity-ui/react-data-table';
3
+ import type {Column as VirtualTableColumn} from '../../components/VirtualTable';
4
+ import {settingsManager} from '../../services/settings';
5
+
6
+ export type Column<T> = VirtualTableColumn<T> & DataTableColumn<T>;
7
+
8
+ export type TableColumnsWidthSetup = Record<string, number>;
9
+
10
+ export type HandleTableColumnsResize = (newSetup: TableColumnsWidthSetup) => void;
11
+
12
+ export const updateColumnsWidth = <T>(
13
+ columns: Column<T>[],
14
+ columnsWidthSetup: TableColumnsWidthSetup,
15
+ ) => {
16
+ return columns.map((column) => {
17
+ if (!column.resizeable) {
18
+ return column;
19
+ }
20
+ return {...column, width: columnsWidthSetup[column.name] ?? column.width};
21
+ });
22
+ };
23
+
24
+ export const useTableResize = (
25
+ localStorageKey: string,
26
+ ): [TableColumnsWidthSetup, HandleTableColumnsResize] => {
27
+ const [tableColumnsWidthSetup, setTableColumnsWidth] = useState<TableColumnsWidthSetup>(() => {
28
+ const setupFromLS = settingsManager.readUserSettingsValue(
29
+ localStorageKey,
30
+ {},
31
+ ) as TableColumnsWidthSetup;
32
+
33
+ return setupFromLS;
34
+ });
35
+
36
+ const handleSetupChange: HandleTableColumnsResize = useCallback(
37
+ (newSetup) => {
38
+ setTableColumnsWidth((previousSetup) => {
39
+ // ResizeObserver callback may be triggered only for currently resized column
40
+ // or for the whole set of columns
41
+ const setup = {
42
+ ...previousSetup,
43
+ ...newSetup,
44
+ };
45
+ settingsManager.setUserSettingsValue(localStorageKey, setup);
46
+ return setup;
47
+ });
48
+ },
49
+ [localStorageKey],
50
+ );
51
+
52
+ return [tableColumnsWidthSetup, handleSetupChange];
53
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-embedded-ui",
3
- "version": "4.32.0",
3
+ "version": "4.33.0",
4
4
  "files": [
5
5
  "dist"
6
6
  ],