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