ydb-embedded-ui 4.20.3 → 4.21.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. package/CHANGELOG.md +14 -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/store/reducers/storage/storage.ts +1 -1
  29. package/dist/utils/hooks/useNodesRequestParams.ts +4 -8
  30. package/dist/utils/nodes.ts +12 -0
  31. package/package.json +1 -1
@@ -0,0 +1,277 @@
1
+ import {useState, useReducer, useRef, useCallback, useEffect} from 'react';
2
+
3
+ import type {IResponseError} from '../../types/api/error';
4
+
5
+ import {TableWithControlsLayout} from '../TableWithControlsLayout/TableWithControlsLayout';
6
+ import {ResponseError} from '../Errors/ResponseError';
7
+
8
+ import type {
9
+ Column,
10
+ OnSort,
11
+ FetchData,
12
+ SortParams,
13
+ RenderControls,
14
+ OnEntry,
15
+ OnLeave,
16
+ GetRowClassName,
17
+ RenderEmptyDataMessage,
18
+ RenderErrorMessage,
19
+ } from './types';
20
+ import {
21
+ createVirtualTableReducer,
22
+ initChunk,
23
+ removeChunk,
24
+ resetChunks,
25
+ setChunkData,
26
+ setChunkError,
27
+ setChunkLoading,
28
+ } from './reducer';
29
+ import {DEFAULT_REQUEST_TIMEOUT, DEFAULT_TABLE_ROW_HEIGHT} from './constants';
30
+ import {TableHead} from './TableHead';
31
+ import {TableChunk} from './TableChunk';
32
+ import {EmptyTableRow} from './TableRow';
33
+ import {useIntersectionObserver} from './useIntersectionObserver';
34
+ import {getArray} from './utils';
35
+ import i18n from './i18n';
36
+ import {b} from './shared';
37
+
38
+ import './VirtualTable.scss';
39
+
40
+ interface VirtualTableProps<T> {
41
+ limit: number;
42
+ fetchData: FetchData<T>;
43
+ columns: Column<T>[];
44
+ getRowClassName?: GetRowClassName<T>;
45
+ rowHeight?: number;
46
+ parentContainer?: Element | null;
47
+ initialSortParams?: SortParams;
48
+ renderControls?: RenderControls;
49
+ renderEmptyDataMessage?: RenderEmptyDataMessage;
50
+ renderErrorMessage?: RenderErrorMessage;
51
+ dependencyArray?: unknown[]; // Fully reload table on params change
52
+ }
53
+
54
+ export const VirtualTable = <T,>({
55
+ limit,
56
+ fetchData,
57
+ columns,
58
+ getRowClassName,
59
+ rowHeight = DEFAULT_TABLE_ROW_HEIGHT,
60
+ parentContainer,
61
+ initialSortParams,
62
+ renderControls,
63
+ renderEmptyDataMessage,
64
+ renderErrorMessage,
65
+ dependencyArray,
66
+ }: VirtualTableProps<T>) => {
67
+ const inited = useRef(false);
68
+ const tableContainer = useRef<HTMLDivElement>(null);
69
+
70
+ const [state, dispatch] = useReducer(createVirtualTableReducer<T>(), {});
71
+
72
+ const [sortParams, setSortParams] = useState<SortParams | undefined>(initialSortParams);
73
+
74
+ const [totalEntities, setTotalEntities] = useState(limit);
75
+ const [foundEntities, setFoundEntities] = useState(0);
76
+
77
+ const [error, setError] = useState<IResponseError>();
78
+
79
+ const [pendingRequests, setPendingRequests] = useState<Record<string, NodeJS.Timeout>>({});
80
+
81
+ const fetchChunkData = useCallback(
82
+ async (id: string) => {
83
+ dispatch(setChunkLoading(id));
84
+
85
+ const timer = setTimeout(async () => {
86
+ const offset = Number(id) * limit;
87
+
88
+ try {
89
+ const response = await fetchData(limit, offset, sortParams);
90
+ const {data, total, found} = response;
91
+
92
+ setTotalEntities(total);
93
+ setFoundEntities(found);
94
+ inited.current = true;
95
+
96
+ dispatch(setChunkData(id, data));
97
+ } catch (err) {
98
+ // Do not set error on cancelled requests
99
+ if ((err as IResponseError)?.isCancelled) {
100
+ return;
101
+ }
102
+
103
+ dispatch(setChunkError(id, err as IResponseError));
104
+ setError(err as IResponseError);
105
+ }
106
+ }, DEFAULT_REQUEST_TIMEOUT);
107
+
108
+ setPendingRequests((reqs) => {
109
+ reqs[id] = timer;
110
+ return reqs;
111
+ });
112
+ },
113
+ [fetchData, limit, sortParams],
114
+ );
115
+
116
+ const onEntry = useCallback<OnEntry>((id) => {
117
+ dispatch(initChunk(id));
118
+ }, []);
119
+
120
+ const onLeave = useCallback<OnLeave>(
121
+ (id) => {
122
+ dispatch(removeChunk(id));
123
+
124
+ // If there is a pending request for the removed chunk, cancel it
125
+ // It made to prevent excessive requests on fast scroll
126
+ if (pendingRequests[id]) {
127
+ const timer = pendingRequests[id];
128
+ window.clearTimeout(timer);
129
+ delete pendingRequests[id];
130
+ }
131
+ },
132
+ [pendingRequests],
133
+ );
134
+
135
+ // Load chunks if they become active
136
+ // This mecanism helps to set chunk active state from different sources, but load data only once
137
+ // Only currently active chunks should be in state so iteration by the whole state shouldn't be a problem
138
+ useEffect(() => {
139
+ for (const id of Object.keys(state)) {
140
+ const chunk = state[Number(id)];
141
+
142
+ if (chunk?.active && !chunk?.loading && !chunk?.wasLoaded) {
143
+ fetchChunkData(id);
144
+ }
145
+ }
146
+ }, [fetchChunkData, state]);
147
+
148
+ // Reset table on filters change
149
+ useEffect(() => {
150
+ // Reset counts, so table unmount unneeded chunks
151
+ setTotalEntities(limit);
152
+ setFoundEntities(0);
153
+ setError(undefined);
154
+
155
+ // Remove all chunks from state
156
+ dispatch(resetChunks());
157
+
158
+ // Reset table state for the controls
159
+ inited.current = false;
160
+
161
+ // If there is a parent, scroll to parent container ref
162
+ // Else scroll to table top
163
+ // It helps to prevent layout shifts, when chunks quantity is changed
164
+ if (parentContainer) {
165
+ parentContainer.scrollTo(0, 0);
166
+ } else {
167
+ tableContainer.current?.scrollTo(0, 0);
168
+ }
169
+
170
+ // Make table start to load data
171
+ dispatch(initChunk('0'));
172
+ }, [dependencyArray, limit, parentContainer]);
173
+
174
+ // Reload currently active chunks
175
+ // Use case - sort params change, so data should be updated, but without chunks unmount
176
+ const reloadCurrentViewport = () => {
177
+ for (const id of Object.keys(state)) {
178
+ if (state[Number(id)]?.active) {
179
+ dispatch(initChunk(id));
180
+ }
181
+ }
182
+ };
183
+
184
+ const handleSort: OnSort = (params) => {
185
+ setSortParams(params);
186
+ reloadCurrentViewport();
187
+ };
188
+
189
+ const observer = useIntersectionObserver({onEntry, onLeave, parentContainer});
190
+
191
+ // Render at least 1 chunk
192
+ const totalLength = foundEntities || limit;
193
+ const chunksCount = Math.ceil(totalLength / limit);
194
+
195
+ const renderChunks = () => {
196
+ if (!observer) {
197
+ return null;
198
+ }
199
+
200
+ return getArray(chunksCount).map((value) => {
201
+ const chunkData = state[value];
202
+
203
+ return (
204
+ <TableChunk
205
+ observer={observer}
206
+ key={value}
207
+ id={value}
208
+ chunkSize={limit}
209
+ rowHeight={rowHeight}
210
+ columns={columns}
211
+ chunkData={chunkData}
212
+ getRowClassName={getRowClassName}
213
+ />
214
+ );
215
+ });
216
+ };
217
+
218
+ const renderData = () => {
219
+ if (inited.current && foundEntities === 0) {
220
+ return (
221
+ <tbody>
222
+ <EmptyTableRow columns={columns}>
223
+ {renderEmptyDataMessage ? renderEmptyDataMessage() : i18n('empty')}
224
+ </EmptyTableRow>
225
+ </tbody>
226
+ );
227
+ }
228
+
229
+ // If first chunk is loaded with the error, display error
230
+ // In case of other chunks table will be inited
231
+ if (!inited.current && error) {
232
+ return (
233
+ <tbody>
234
+ <EmptyTableRow columns={columns}>
235
+ {renderErrorMessage ? (
236
+ renderErrorMessage(error)
237
+ ) : (
238
+ <ResponseError error={error} />
239
+ )}
240
+ </EmptyTableRow>
241
+ </tbody>
242
+ );
243
+ }
244
+
245
+ return renderChunks();
246
+ };
247
+
248
+ const renderTable = () => {
249
+ return (
250
+ <table className={b('table')}>
251
+ <TableHead columns={columns} onSort={handleSort} />
252
+ {renderData()}
253
+ </table>
254
+ );
255
+ };
256
+
257
+ const renderContent = () => {
258
+ if (renderControls) {
259
+ return (
260
+ <TableWithControlsLayout>
261
+ <TableWithControlsLayout.Controls>
262
+ {renderControls({inited: inited.current, totalEntities, foundEntities})}
263
+ </TableWithControlsLayout.Controls>
264
+ <TableWithControlsLayout.Table>{renderTable()}</TableWithControlsLayout.Table>
265
+ </TableWithControlsLayout>
266
+ );
267
+ }
268
+
269
+ return renderTable();
270
+ };
271
+
272
+ return (
273
+ <div ref={tableContainer} className={b(null)}>
274
+ {renderContent()}
275
+ </div>
276
+ );
277
+ };
@@ -0,0 +1,17 @@
1
+ export const LEFT = 'left';
2
+ export const CENTER = 'center';
3
+ export const RIGHT = 'right';
4
+
5
+ export const DEFAULT_ALIGN = LEFT;
6
+
7
+ export const ASCENDING = 1;
8
+ export const DESCENDING = -1;
9
+
10
+ export const DEFAULT_SORT_ORDER = DESCENDING;
11
+
12
+ // Time in ms after which request will be sent
13
+ export const DEFAULT_REQUEST_TIMEOUT = 200;
14
+
15
+ export const DEFAULT_TABLE_ROW_HEIGHT = 40;
16
+
17
+ export const DEFAULT_INTERSECTION_OBSERVER_MARGIN = '100%';
@@ -0,0 +1,3 @@
1
+ {
2
+ "empty": "No data"
3
+ }
@@ -0,0 +1,11 @@
1
+ import {i18n, Lang} from '../../../utils/i18n';
2
+
3
+ import en from './en.json';
4
+ import ru from './ru.json';
5
+
6
+ const COMPONENT = 'ydb-virtual-table';
7
+
8
+ i18n.registerKeyset(Lang.En, COMPONENT, en);
9
+ i18n.registerKeyset(Lang.Ru, COMPONENT, ru);
10
+
11
+ export default i18n.keyset(COMPONENT);
@@ -0,0 +1,3 @@
1
+ {
2
+ "empty": "Нет данных"
3
+ }
@@ -0,0 +1,3 @@
1
+ export * from './constants';
2
+ export * from './types';
3
+ export * from './VirtualTable';
@@ -0,0 +1,143 @@
1
+ import type {Reducer} from 'react';
2
+
3
+ import type {IResponseError} from '../../types/api/error';
4
+
5
+ import type {Chunk} from './types';
6
+
7
+ const INIT_CHUNK = 'infiniteTable/INIT_CHUNK';
8
+ const REMOVE_CHUNK = 'infiniteTable/REMOVE_CHUNK';
9
+ const SET_CHUNK_LOADING = 'infiniteTable/SET_CHUNK_LOADING';
10
+ const SET_CHUNK_DATA = 'infiniteTable/SET_CHUNK_DATA';
11
+ const SET_CHUNK_ERROR = 'infiniteTable/SET_CHUNK_ERROR';
12
+ const RESET_CHUNKS = 'infiniteTable/RESET_CHUNKS';
13
+
14
+ type VirtualTableState<T> = Record<string, Chunk<T> | undefined>;
15
+
16
+ // Intermediary type to pass to ReducerAction (because ReturnType cannot correctly convert generics)
17
+ interface SetChunkDataAction<T> {
18
+ type: typeof SET_CHUNK_DATA;
19
+ data: {
20
+ id: string;
21
+ data: T[];
22
+ };
23
+ }
24
+
25
+ export const setChunkData = <T>(id: string, data: T[]): SetChunkDataAction<T> => {
26
+ return {
27
+ type: SET_CHUNK_DATA,
28
+ data: {id, data},
29
+ } as const;
30
+ };
31
+
32
+ export const setChunkError = (id: string, error: IResponseError) => {
33
+ return {
34
+ type: SET_CHUNK_ERROR,
35
+ data: {id, error},
36
+ } as const;
37
+ };
38
+
39
+ export const initChunk = (id: string) => {
40
+ return {
41
+ type: INIT_CHUNK,
42
+ data: {id},
43
+ } as const;
44
+ };
45
+
46
+ export const setChunkLoading = (id: string) => {
47
+ return {
48
+ type: SET_CHUNK_LOADING,
49
+ data: {id},
50
+ } as const;
51
+ };
52
+
53
+ export const removeChunk = (id: string) => {
54
+ return {
55
+ type: REMOVE_CHUNK,
56
+ data: {id},
57
+ } as const;
58
+ };
59
+
60
+ export const resetChunks = () => {
61
+ return {
62
+ type: RESET_CHUNKS,
63
+ } as const;
64
+ };
65
+
66
+ type VirtualTableAction<T> =
67
+ | SetChunkDataAction<T>
68
+ | ReturnType<typeof setChunkError>
69
+ | ReturnType<typeof initChunk>
70
+ | ReturnType<typeof setChunkLoading>
71
+ | ReturnType<typeof removeChunk>
72
+ | ReturnType<typeof resetChunks>;
73
+
74
+ // Reducer wrapped in additional function to pass generic type
75
+ export const createVirtualTableReducer =
76
+ <T>(): Reducer<VirtualTableState<T>, VirtualTableAction<T>> =>
77
+ (state, action) => {
78
+ switch (action.type) {
79
+ case SET_CHUNK_DATA: {
80
+ const {id, data} = action.data;
81
+
82
+ return {
83
+ ...state,
84
+ [id]: {
85
+ loading: false,
86
+ wasLoaded: true,
87
+ active: true,
88
+ data,
89
+ },
90
+ };
91
+ }
92
+ case SET_CHUNK_ERROR: {
93
+ const {id, error} = action.data;
94
+
95
+ return {
96
+ ...state,
97
+ [id]: {
98
+ loading: false,
99
+ wasLoaded: true,
100
+ active: true,
101
+ error,
102
+ },
103
+ };
104
+ }
105
+ case INIT_CHUNK: {
106
+ const {id} = action.data;
107
+
108
+ return {
109
+ ...state,
110
+ [id]: {
111
+ loading: false,
112
+ wasLoaded: false,
113
+ active: true,
114
+ },
115
+ };
116
+ }
117
+ case SET_CHUNK_LOADING: {
118
+ const {id} = action.data;
119
+
120
+ return {
121
+ ...state,
122
+ [id]: {
123
+ loading: true,
124
+ wasLoaded: false,
125
+ active: true,
126
+ },
127
+ };
128
+ }
129
+ case REMOVE_CHUNK: {
130
+ const {id} = action.data;
131
+
132
+ const newState = {...state};
133
+ delete newState[id];
134
+
135
+ return newState;
136
+ }
137
+ case RESET_CHUNKS: {
138
+ return {};
139
+ }
140
+ default:
141
+ return state;
142
+ }
143
+ };
@@ -0,0 +1,3 @@
1
+ import cn from 'bem-cn-lite';
2
+
3
+ export const b = cn('ydb-virtual-table');
@@ -0,0 +1,60 @@
1
+ import type {ReactNode} from 'react';
2
+
3
+ import type {IResponseError} from '../../types/api/error';
4
+
5
+ import {ASCENDING, CENTER, DESCENDING, LEFT, RIGHT} from './constants';
6
+
7
+ export interface Chunk<T> {
8
+ active: boolean;
9
+ loading: boolean;
10
+ wasLoaded: boolean;
11
+ data?: T[];
12
+ error?: IResponseError;
13
+ }
14
+
15
+ export type GetChunk<T> = (id: number) => Chunk<T> | undefined;
16
+
17
+ export type OnEntry = (id: string) => void;
18
+ export type OnLeave = (id: string) => void;
19
+
20
+ export type AlignType = typeof LEFT | typeof RIGHT | typeof CENTER;
21
+ export type SortOrderType = typeof ASCENDING | typeof DESCENDING;
22
+
23
+ export type SortParams = {columnId?: string; sortOrder?: SortOrderType};
24
+ export type OnSort = (params: SortParams) => void;
25
+
26
+ export interface Column<T> {
27
+ name: string;
28
+ header?: ReactNode;
29
+ className?: string;
30
+ sortable?: boolean;
31
+ render: (props: {row: T; index: number}) => ReactNode;
32
+ width: number;
33
+ align: AlignType;
34
+ }
35
+
36
+ export interface VirtualTableData<T> {
37
+ data: T[];
38
+ total: number;
39
+ found: number;
40
+ }
41
+
42
+ export type FetchData<T> = (
43
+ limit: number,
44
+ offset: number,
45
+ sortParams?: SortParams,
46
+ ) => Promise<VirtualTableData<T>>;
47
+
48
+ export type OnError = (error?: IResponseError) => void;
49
+
50
+ interface ControlsParams {
51
+ totalEntities: number;
52
+ foundEntities: number;
53
+ inited: boolean;
54
+ }
55
+
56
+ export type RenderControls = (params: ControlsParams) => ReactNode;
57
+ export type RenderEmptyDataMessage = () => ReactNode;
58
+ export type RenderErrorMessage = (error: IResponseError) => ReactNode;
59
+
60
+ export type GetRowClassName<T> = (row: T) => string | undefined;
@@ -0,0 +1,42 @@
1
+ import {useEffect, useRef} from 'react';
2
+
3
+ import type {OnEntry, OnLeave} from './types';
4
+ import {DEFAULT_INTERSECTION_OBSERVER_MARGIN} from './constants';
5
+
6
+ interface UseIntersectionObserverProps {
7
+ onEntry: OnEntry;
8
+ onLeave: OnLeave;
9
+ parentContainer?: Element | null;
10
+ }
11
+
12
+ export const useIntersectionObserver = ({
13
+ onEntry,
14
+ onLeave,
15
+ parentContainer,
16
+ }: UseIntersectionObserverProps) => {
17
+ const observer = useRef<IntersectionObserver>();
18
+
19
+ useEffect(() => {
20
+ const callback = (entries: IntersectionObserverEntry[]) => {
21
+ entries.forEach((entry) => {
22
+ if (entry.isIntersecting) {
23
+ onEntry(entry.target.id);
24
+ } else {
25
+ onLeave(entry.target.id);
26
+ }
27
+ });
28
+ };
29
+
30
+ observer.current = new IntersectionObserver(callback, {
31
+ root: parentContainer,
32
+ rootMargin: DEFAULT_INTERSECTION_OBSERVER_MARGIN,
33
+ });
34
+
35
+ return () => {
36
+ observer.current?.disconnect();
37
+ observer.current = undefined;
38
+ };
39
+ }, [parentContainer, onEntry, onLeave]);
40
+
41
+ return observer.current;
42
+ };
@@ -0,0 +1,3 @@
1
+ export const getArray = (arrayLength: number) => {
2
+ return [...Array(arrayLength).keys()];
3
+ };
@@ -123,7 +123,8 @@ body,
123
123
  color: var(--yc-color-text-danger);
124
124
  }
125
125
 
126
- .data-table__row:hover .entity-status__clipboard-button {
126
+ .data-table__row:hover .entity-status__clipboard-button,
127
+ .ydb-virtual-table__row:hover .entity-status__clipboard-button {
127
128
  display: flex;
128
129
  }
129
130
 
@@ -1,4 +1,4 @@
1
- import {useEffect, useMemo} from 'react';
1
+ import {useEffect, useMemo, useRef} from 'react';
2
2
  import {useLocation, useRouteMatch} from 'react-router';
3
3
  import {useDispatch} from 'react-redux';
4
4
  import cn from 'bem-cn-lite';
@@ -18,11 +18,13 @@ import {setHeaderBreadcrumbs} from '../../store/reducers/header/header';
18
18
  import {getClusterInfo} from '../../store/reducers/cluster/cluster';
19
19
  import {getClusterNodes} from '../../store/reducers/clusterNodes/clusterNodes';
20
20
  import {parseNodesToVersionsValues, parseVersionsToVersionToColorMap} from '../../utils/versions';
21
- import {useAutofetcher, useTypedSelector} from '../../utils/hooks';
21
+ import {useAutofetcher, useSetting, useTypedSelector} from '../../utils/hooks';
22
+ import {USE_BACKEND_PARAMS_FOR_TABLES_KEY} from '../../utils/constants';
22
23
 
23
24
  import {InternalLink} from '../../components/InternalLink';
24
25
  import {Tenants} from '../Tenants/Tenants';
25
26
  import {Nodes} from '../Nodes/Nodes';
27
+ import {VirtualNodes} from '../Nodes/VirtualNodes';
26
28
  import {Storage} from '../Storage/Storage';
27
29
  import {Versions} from '../Versions/Versions';
28
30
 
@@ -46,11 +48,15 @@ function Cluster({
46
48
  additionalNodesProps,
47
49
  additionalVersionsProps,
48
50
  }: ClusterProps) {
51
+ const container = useRef<HTMLDivElement>(null);
52
+
49
53
  const dispatch = useDispatch();
50
54
 
51
55
  const match = useRouteMatch<{activeTab: string}>(routes.cluster);
52
56
  const {activeTab = clusterTabsIds.tenants} = match?.params || {};
53
57
 
58
+ const [useVirtualNodes] = useSetting<boolean>(USE_BACKEND_PARAMS_FOR_TABLES_KEY);
59
+
54
60
  const location = useLocation();
55
61
  const queryParams = qs.parse(location.search, {
56
62
  ignoreQueryPrefix: true,
@@ -104,7 +110,14 @@ function Cluster({
104
110
  return <Tenants additionalTenantsProps={additionalTenantsProps} />;
105
111
  }
106
112
  case clusterTabsIds.nodes: {
107
- return <Nodes additionalNodesProps={additionalNodesProps} />;
113
+ return useVirtualNodes ? (
114
+ <VirtualNodes
115
+ parentContainer={container.current}
116
+ additionalNodesProps={additionalNodesProps}
117
+ />
118
+ ) : (
119
+ <Nodes additionalNodesProps={additionalNodesProps} />
120
+ );
108
121
  }
109
122
  case clusterTabsIds.storage: {
110
123
  return <Storage additionalNodesProps={additionalNodesProps} />;
@@ -119,7 +132,7 @@ function Cluster({
119
132
  };
120
133
 
121
134
  return (
122
- <div className={b()}>
135
+ <div className={b()} ref={container}>
123
136
  <ClusterInfo
124
137
  cluster={cluster}
125
138
  versionsValues={versionsValues}