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.
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
@@ -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}