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.
- package/CHANGELOG.md +7 -0
- package/dist/components/EmptyState/EmptyState.scss +0 -1
- package/dist/components/ProgressViewer/ProgressViewer.scss +1 -0
- package/dist/components/TableWithControlsLayout/TableWithControlsLayout.scss +4 -0
- package/dist/components/VirtualTable/TableChunk.tsx +84 -0
- package/dist/components/VirtualTable/TableHead.tsx +139 -0
- package/dist/components/VirtualTable/TableRow.tsx +91 -0
- package/dist/components/VirtualTable/VirtualTable.scss +146 -0
- package/dist/components/VirtualTable/VirtualTable.tsx +277 -0
- package/dist/components/VirtualTable/constants.ts +17 -0
- package/dist/components/VirtualTable/i18n/en.json +3 -0
- package/dist/components/VirtualTable/i18n/index.ts +11 -0
- package/dist/components/VirtualTable/i18n/ru.json +3 -0
- package/dist/components/VirtualTable/index.ts +3 -0
- package/dist/components/VirtualTable/reducer.ts +143 -0
- package/dist/components/VirtualTable/shared.ts +3 -0
- package/dist/components/VirtualTable/types.ts +60 -0
- package/dist/components/VirtualTable/useIntersectionObserver.ts +42 -0
- package/dist/components/VirtualTable/utils.ts +3 -0
- package/dist/containers/App/App.scss +2 -1
- package/dist/containers/Cluster/Cluster.tsx +17 -4
- package/dist/containers/Nodes/Nodes.tsx +4 -20
- package/dist/containers/Nodes/VirtualNodes.tsx +146 -0
- package/dist/containers/Nodes/getNodes.ts +26 -0
- package/dist/containers/Nodes/getNodesColumns.tsx +49 -39
- package/dist/containers/UserSettings/i18n/en.json +2 -2
- package/dist/containers/UserSettings/i18n/ru.json +2 -2
- package/dist/utils/hooks/useNodesRequestParams.ts +4 -8
- package/dist/utils/nodes.ts +12 -0
- package/package.json +1 -1
@@ -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,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,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,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
|
+
};
|
@@ -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
|
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}
|