ydb-embedded-ui 4.20.4 → 4.21.1
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 +20 -0
- package/dist/components/EmptyState/EmptyState.scss +0 -1
- package/dist/components/NodeHostWrapper/NodeHostWrapper.scss +2 -1
- package/dist/components/ProgressViewer/ProgressViewer.scss +1 -0
- package/dist/components/QueryResultTable/QueryResultTable.tsx +7 -3
- 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/Node/Node.tsx +9 -17
- package/dist/containers/Node/NodeStructure/NodeStructure.tsx +8 -30
- package/dist/containers/Node/NodeStructure/Pdisk.tsx +29 -18
- package/dist/containers/Node/i18n/en.json +4 -0
- package/dist/containers/Node/i18n/index.ts +11 -0
- package/dist/containers/Node/i18n/ru.json +4 -0
- package/dist/containers/Nodes/Nodes.tsx +7 -28
- 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/Tablets/Tablets.tsx +3 -8
- package/dist/containers/Tenant/Diagnostics/Diagnostics.tsx +0 -1
- package/dist/containers/UserSettings/i18n/en.json +4 -4
- package/dist/containers/UserSettings/i18n/ru.json +4 -4
- package/dist/containers/UserSettings/settings.ts +5 -6
- package/dist/store/reducers/nodes/nodes.ts +2 -2
- package/dist/store/reducers/nodes/types.ts +1 -0
- package/dist/store/reducers/settings/settings.ts +3 -3
- package/dist/utils/developerUI.ts +32 -0
- package/dist/utils/hooks/useNodesRequestParams.ts +4 -8
- package/dist/utils/nodes.ts +12 -0
- package/dist/utils/query.ts +8 -1
- package/package.json +1 -1
@@ -2,7 +2,6 @@ import * as React from 'react';
|
|
2
2
|
import {useLocation, useRouteMatch} from 'react-router';
|
3
3
|
import cn from 'bem-cn-lite';
|
4
4
|
import {useDispatch} from 'react-redux';
|
5
|
-
import _ from 'lodash';
|
6
5
|
|
7
6
|
import {Tabs} from '@gravity-ui/uikit';
|
8
7
|
import {Link} from 'react-router-dom';
|
@@ -52,21 +51,22 @@ function Node(props: NodeProps) {
|
|
52
51
|
const {tenantName: tenantNameFromQuery} = parseQuery(location);
|
53
52
|
|
54
53
|
const {activeTabVerified, nodeTabs} = React.useMemo(() => {
|
55
|
-
const hasStorage =
|
56
|
-
|
54
|
+
const hasStorage = node?.Roles?.find((el) => el === STORAGE_ROLE);
|
55
|
+
|
56
|
+
let actualActiveTab = activeTab;
|
57
57
|
if (!hasStorage && activeTab === STORAGE) {
|
58
|
-
|
58
|
+
actualActiveTab = OVERVIEW;
|
59
59
|
}
|
60
60
|
const nodePages = hasStorage ? NODE_PAGES : NODE_PAGES.filter((el) => el.id !== STORAGE);
|
61
61
|
|
62
|
-
const
|
62
|
+
const actualNodeTabs = nodePages.map((page) => {
|
63
63
|
return {
|
64
64
|
...page,
|
65
65
|
title: page.name,
|
66
66
|
};
|
67
67
|
});
|
68
68
|
|
69
|
-
return {activeTabVerified, nodeTabs};
|
69
|
+
return {activeTabVerified: actualActiveTab, nodeTabs: actualNodeTabs};
|
70
70
|
}, [activeTab, node]);
|
71
71
|
|
72
72
|
React.useEffect(() => {
|
@@ -100,13 +100,13 @@ function Node(props: NodeProps) {
|
|
100
100
|
size="l"
|
101
101
|
items={nodeTabs}
|
102
102
|
activeTab={activeTabVerified}
|
103
|
-
wrapTo={({id},
|
103
|
+
wrapTo={({id}, tabNode) => (
|
104
104
|
<Link
|
105
105
|
to={createHref(routes.node, {id: nodeId, activeTab: id})}
|
106
106
|
key={id}
|
107
107
|
className={b('tab')}
|
108
108
|
>
|
109
|
-
{
|
109
|
+
{tabNode}
|
110
110
|
</Link>
|
111
111
|
)}
|
112
112
|
allowNotSelected={true}
|
@@ -115,8 +115,6 @@ function Node(props: NodeProps) {
|
|
115
115
|
);
|
116
116
|
};
|
117
117
|
const renderTabContent = () => {
|
118
|
-
const {additionalNodesProps} = props;
|
119
|
-
|
120
118
|
switch (activeTab) {
|
121
119
|
case STORAGE: {
|
122
120
|
return (
|
@@ -134,13 +132,7 @@ function Node(props: NodeProps) {
|
|
134
132
|
}
|
135
133
|
|
136
134
|
case STRUCTURE: {
|
137
|
-
return (
|
138
|
-
<NodeStructure
|
139
|
-
className={b('node-page-wrapper')}
|
140
|
-
nodeId={nodeId}
|
141
|
-
additionalNodesProps={additionalNodesProps}
|
142
|
-
/>
|
143
|
-
);
|
135
|
+
return <NodeStructure className={b('node-page-wrapper')} nodeId={nodeId} />;
|
144
136
|
}
|
145
137
|
default:
|
146
138
|
return false;
|
@@ -1,7 +1,7 @@
|
|
1
|
-
import {useEffect, useRef
|
1
|
+
import {useEffect, useRef} from 'react';
|
2
2
|
import {useDispatch} from 'react-redux';
|
3
3
|
import url from 'url';
|
4
|
-
import
|
4
|
+
import {isEmpty} from 'lodash/fp';
|
5
5
|
|
6
6
|
import cn from 'bem-cn-lite';
|
7
7
|
|
@@ -13,15 +13,13 @@ import {selectNodeStructure} from '../../../store/reducers/node/selectors';
|
|
13
13
|
import {AutoFetcher} from '../../../utils/autofetcher';
|
14
14
|
import {useTypedSelector} from '../../../utils/hooks';
|
15
15
|
|
16
|
-
import type {AdditionalNodesProps} from '../../../types/additionalProps';
|
17
|
-
|
18
16
|
import {PDisk} from './Pdisk';
|
19
17
|
|
20
18
|
import './NodeStructure.scss';
|
21
19
|
|
22
20
|
const b = cn('kv-node-structure');
|
23
21
|
|
24
|
-
export function valueIsDefined(value:
|
22
|
+
export function valueIsDefined<T>(value: T | null | undefined): value is T {
|
25
23
|
return value !== null && value !== undefined;
|
26
24
|
}
|
27
25
|
|
@@ -32,24 +30,16 @@ function generateId({type, id}: {type: 'pdisk' | 'vdisk'; id: string}) {
|
|
32
30
|
interface NodeStructureProps {
|
33
31
|
nodeId: string;
|
34
32
|
className?: string;
|
35
|
-
additionalNodesProps?: AdditionalNodesProps;
|
36
33
|
}
|
37
34
|
|
38
35
|
const autofetcher = new AutoFetcher();
|
39
36
|
|
40
|
-
function NodeStructure({nodeId, className
|
37
|
+
function NodeStructure({nodeId, className}: NodeStructureProps) {
|
41
38
|
const dispatch = useDispatch();
|
42
39
|
|
43
40
|
const nodeStructure = useTypedSelector(selectNodeStructure);
|
44
41
|
|
45
42
|
const {loadingStructure, wasLoadedStructure} = useTypedSelector((state) => state.node);
|
46
|
-
const nodeData = useTypedSelector((state) => state.node?.data?.SystemStateInfo?.[0]);
|
47
|
-
|
48
|
-
const nodeHref = useMemo(() => {
|
49
|
-
return additionalNodesProps?.getNodeRef
|
50
|
-
? additionalNodesProps.getNodeRef(nodeData)
|
51
|
-
: undefined;
|
52
|
-
}, [nodeData, additionalNodesProps]);
|
53
43
|
|
54
44
|
const {pdiskId: pdiskIdFromUrl, vdiskId: vdiskIdFromUrl} = url.parse(
|
55
45
|
window.location.href,
|
@@ -57,23 +47,11 @@ function NodeStructure({nodeId, className, additionalNodesProps}: NodeStructureP
|
|
57
47
|
).query;
|
58
48
|
|
59
49
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
60
|
-
const scrollContainer = scrollContainerRef.current;
|
61
50
|
|
62
51
|
const isReady = useRef(false);
|
63
52
|
|
64
53
|
const scrolled = useRef(false);
|
65
54
|
|
66
|
-
useEffect(() => {
|
67
|
-
return () => {
|
68
|
-
if (scrollContainer) {
|
69
|
-
scrollContainer.scrollTo({
|
70
|
-
behavior: 'smooth',
|
71
|
-
top: 0,
|
72
|
-
});
|
73
|
-
}
|
74
|
-
};
|
75
|
-
}, []);
|
76
|
-
|
77
55
|
useEffect(() => {
|
78
56
|
dispatch(getNodeStructure(nodeId));
|
79
57
|
autofetcher.start();
|
@@ -87,13 +65,13 @@ function NodeStructure({nodeId, className, additionalNodesProps}: NodeStructureP
|
|
87
65
|
}, [nodeId, dispatch]);
|
88
66
|
|
89
67
|
useEffect(() => {
|
90
|
-
if (!
|
68
|
+
if (!isEmpty(nodeStructure) && scrollContainerRef.current) {
|
91
69
|
isReady.current = true;
|
92
70
|
}
|
93
71
|
}, [nodeStructure]);
|
94
72
|
|
95
73
|
useEffect(() => {
|
96
|
-
if (isReady.current && !scrolled.current &&
|
74
|
+
if (isReady.current && !scrolled.current && scrollContainerRef.current) {
|
97
75
|
const element = document.getElementById(
|
98
76
|
generateId({type: 'pdisk', id: pdiskIdFromUrl as string}),
|
99
77
|
);
|
@@ -112,7 +90,7 @@ function NodeStructure({nodeId, className, additionalNodesProps}: NodeStructureP
|
|
112
90
|
}
|
113
91
|
|
114
92
|
if (element) {
|
115
|
-
|
93
|
+
scrollContainerRef.current.scrollTo({
|
116
94
|
behavior: 'smooth',
|
117
95
|
// should subtract 20 to avoid sticking the element to tabs
|
118
96
|
top: scrollToVdisk ? scrollToVdisk : element.offsetTop,
|
@@ -136,7 +114,7 @@ function NodeStructure({nodeId, className, additionalNodesProps}: NodeStructureP
|
|
136
114
|
id={generateId({type: 'pdisk', id: pDiskId})}
|
137
115
|
unfolded={pdiskIdFromUrl === pDiskId}
|
138
116
|
selectedVdiskId={vdiskIdFromUrl as string}
|
139
|
-
|
117
|
+
nodeId={nodeId}
|
140
118
|
/>
|
141
119
|
))
|
142
120
|
: renderStub();
|
@@ -12,15 +12,17 @@ import type {
|
|
12
12
|
PreparedStructureVDisk,
|
13
13
|
} from '../../../store/reducers/node/types';
|
14
14
|
import {EVDiskState} from '../../../types/api/vdisk';
|
15
|
-
import {bytesToGB
|
15
|
+
import {bytesToGB} from '../../../utils/utils';
|
16
16
|
import {formatStorageValuesToGb} from '../../../utils/dataFormatters/dataFormatters';
|
17
17
|
import {getPDiskType} from '../../../utils/pdisk';
|
18
18
|
import {DEFAULT_TABLE_SETTINGS} from '../../../utils/constants';
|
19
|
+
import {createPDiskDeveloperUILink, createVDiskDeveloperUILink} from '../../../utils/developerUI';
|
19
20
|
import EntityStatus from '../../../components/EntityStatus/EntityStatus';
|
20
21
|
import InfoViewer, {type InfoViewerItem} from '../../../components/InfoViewer/InfoViewer';
|
21
22
|
import {ProgressViewer} from '../../../components/ProgressViewer/ProgressViewer';
|
22
23
|
import {Icon} from '../../../components/Icon';
|
23
24
|
|
25
|
+
import i18n from '../i18n';
|
24
26
|
import {Vdisk} from './Vdisk';
|
25
27
|
import {valueIsDefined} from './NodeStructure';
|
26
28
|
import {PDiskTitleBadge} from './PDiskTitleBadge';
|
@@ -32,7 +34,7 @@ interface PDiskProps {
|
|
32
34
|
unfolded?: boolean;
|
33
35
|
id: string;
|
34
36
|
selectedVdiskId?: string;
|
35
|
-
|
37
|
+
nodeId: string | number;
|
36
38
|
}
|
37
39
|
|
38
40
|
enum VDiskTableColumnsIds {
|
@@ -54,11 +56,11 @@ const vDiskTableColumnsNames: Record<VDiskTableColumnsIdsValues, string> = {
|
|
54
56
|
function getColumns({
|
55
57
|
pDiskId,
|
56
58
|
selectedVdiskId,
|
57
|
-
|
59
|
+
nodeId,
|
58
60
|
}: {
|
59
61
|
pDiskId: number | undefined;
|
60
62
|
selectedVdiskId?: string;
|
61
|
-
|
63
|
+
nodeId?: string | number;
|
62
64
|
}) {
|
63
65
|
const columns: Column<PreparedStructureVDisk>[] = [
|
64
66
|
{
|
@@ -66,26 +68,31 @@ function getColumns({
|
|
66
68
|
header: vDiskTableColumnsNames[VDiskTableColumnsIds.slotId],
|
67
69
|
width: 100,
|
68
70
|
render: ({row}) => {
|
69
|
-
|
71
|
+
const vDiskSlotId = row.VDiskSlotId;
|
72
|
+
let vdiskInternalViewerLink = null;
|
70
73
|
|
71
|
-
if (
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
74
|
+
if (
|
75
|
+
valueIsDefined(nodeId) &&
|
76
|
+
valueIsDefined(pDiskId) &&
|
77
|
+
valueIsDefined(vDiskSlotId)
|
78
|
+
) {
|
79
|
+
vdiskInternalViewerLink = createVDiskDeveloperUILink({
|
80
|
+
nodeId,
|
81
|
+
pDiskId,
|
82
|
+
vDiskSlotId,
|
83
|
+
});
|
78
84
|
}
|
79
85
|
|
80
86
|
return (
|
81
87
|
<div className={b('vdisk-id', {selected: row.id === selectedVdiskId})}>
|
82
|
-
<span>{
|
88
|
+
<span>{vDiskSlotId}</span>
|
83
89
|
{vdiskInternalViewerLink && (
|
84
90
|
<Button
|
85
91
|
size="s"
|
86
92
|
className={b('external-button', {hidden: true})}
|
87
93
|
href={vdiskInternalViewerLink}
|
88
94
|
target="_blank"
|
95
|
+
title={i18n('vdisk.developer-ui-button-title')}
|
89
96
|
>
|
90
97
|
<Icon name="external" />
|
91
98
|
</Button>
|
@@ -156,7 +163,7 @@ export function PDisk({
|
|
156
163
|
id,
|
157
164
|
data,
|
158
165
|
selectedVdiskId,
|
159
|
-
|
166
|
+
nodeId,
|
160
167
|
unfolded: unfoldedFromProps,
|
161
168
|
}: PDiskProps) {
|
162
169
|
const [unfolded, setUnfolded] = useState(unfoldedFromProps ?? false);
|
@@ -190,7 +197,7 @@ export function PDisk({
|
|
190
197
|
<DataTable
|
191
198
|
theme="yandex-cloud"
|
192
199
|
data={vDisks}
|
193
|
-
columns={getColumns({
|
200
|
+
columns={getColumns({nodeId, pDiskId: PDiskId, selectedVdiskId})}
|
194
201
|
settings={{...DEFAULT_TABLE_SETTINGS, dynamicRender: false}}
|
195
202
|
rowClassName={(row) => {
|
196
203
|
return row.id === selectedVdiskId ? b('selected-vdisk') : '';
|
@@ -203,10 +210,13 @@ export function PDisk({
|
|
203
210
|
if (isEmpty(data)) {
|
204
211
|
return <div>No information about PDisk</div>;
|
205
212
|
}
|
206
|
-
let pDiskInternalViewerLink =
|
213
|
+
let pDiskInternalViewerLink = null;
|
207
214
|
|
208
|
-
if (
|
209
|
-
pDiskInternalViewerLink
|
215
|
+
if (valueIsDefined(PDiskId) && valueIsDefined(nodeId)) {
|
216
|
+
pDiskInternalViewerLink = createPDiskDeveloperUILink({
|
217
|
+
nodeId,
|
218
|
+
pDiskId: PDiskId,
|
219
|
+
});
|
210
220
|
}
|
211
221
|
|
212
222
|
const pdiskInfo: InfoViewerItem[] = [
|
@@ -222,6 +232,7 @@ export function PDisk({
|
|
222
232
|
href={pDiskInternalViewerLink}
|
223
233
|
target="_blank"
|
224
234
|
view="flat-secondary"
|
235
|
+
title={i18n('pdisk.developer-ui-button-title')}
|
225
236
|
>
|
226
237
|
<Icon name="external" />
|
227
238
|
</Button>
|
@@ -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-node-page';
|
7
|
+
|
8
|
+
i18n.registerKeyset(Lang.En, COMPONENT, en);
|
9
|
+
i18n.registerKeyset(Lang.Ru, COMPONENT, ru);
|
10
|
+
|
11
|
+
export default i18n.keyset(COMPONENT);
|
@@ -5,7 +5,6 @@ import {useDispatch} from 'react-redux';
|
|
5
5
|
import DataTable from '@gravity-ui/react-data-table';
|
6
6
|
import {ASCENDING} from '@gravity-ui/react-data-table/build/esm/lib/constants';
|
7
7
|
|
8
|
-
import type {EPathType} from '../../types/api/schema';
|
9
8
|
import type {ProblemFilterValue} from '../../store/reducers/settings/types';
|
10
9
|
import type {NodesSortParams} from '../../store/reducers/nodes/types';
|
11
10
|
|
@@ -19,13 +18,7 @@ import {TableWithControlsLayout} from '../../components/TableWithControlsLayout/
|
|
19
18
|
import {ResponseError} from '../../components/Errors/ResponseError';
|
20
19
|
|
21
20
|
import {DEFAULT_TABLE_SETTINGS, USE_NODES_ENDPOINT_IN_DIAGNOSTICS_KEY} from '../../utils/constants';
|
22
|
-
import {
|
23
|
-
useAutofetcher,
|
24
|
-
useSetting,
|
25
|
-
useTypedSelector,
|
26
|
-
useNodesRequestParams,
|
27
|
-
useTableSort,
|
28
|
-
} from '../../utils/hooks';
|
21
|
+
import {useAutofetcher, useSetting, useTypedSelector, useTableSort} from '../../utils/hooks';
|
29
22
|
import {
|
30
23
|
isSortableNodesProperty,
|
31
24
|
isUnavailableNode,
|
@@ -45,8 +38,6 @@ import {selectFilteredNodes} from '../../store/reducers/nodes/selectors';
|
|
45
38
|
import {changeFilter, ProblemFilterValues} from '../../store/reducers/settings/settings';
|
46
39
|
import type {AdditionalNodesProps} from '../../types/additionalProps';
|
47
40
|
|
48
|
-
import {isDatabaseEntityType} from '../Tenant/utils/schema';
|
49
|
-
|
50
41
|
import {getNodesColumns} from './getNodesColumns';
|
51
42
|
|
52
43
|
import './Nodes.scss';
|
@@ -57,11 +48,10 @@ const b = cn('ydb-nodes');
|
|
57
48
|
|
58
49
|
interface NodesProps {
|
59
50
|
path?: string;
|
60
|
-
type?: EPathType;
|
61
51
|
additionalNodesProps?: AdditionalNodesProps;
|
62
52
|
}
|
63
53
|
|
64
|
-
export const Nodes = ({path,
|
54
|
+
export const Nodes = ({path, additionalNodesProps = {}}: NodesProps) => {
|
65
55
|
const dispatch = useDispatch();
|
66
56
|
|
67
57
|
const isClusterNodes = !path;
|
@@ -90,31 +80,20 @@ export const Nodes = ({path, type, additionalNodesProps = {}}: NodesProps) => {
|
|
90
80
|
|
91
81
|
const [useNodesEndpoint] = useSetting(USE_NODES_ENDPOINT_IN_DIAGNOSTICS_KEY);
|
92
82
|
|
93
|
-
const requestParams = useNodesRequestParams({
|
94
|
-
filter: searchValue,
|
95
|
-
problemFilter,
|
96
|
-
nodesUptimeFilter,
|
97
|
-
sortOrder,
|
98
|
-
sortValue,
|
99
|
-
});
|
100
|
-
|
101
83
|
const fetchNodes = useCallback(
|
102
84
|
(isBackground) => {
|
103
85
|
if (!isBackground) {
|
104
86
|
dispatch(setDataWasNotLoaded());
|
105
87
|
}
|
106
88
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
// since /nodes can return data only for tenants
|
111
|
-
if (path && (!useNodesEndpoint || !isDatabaseEntityType(type))) {
|
112
|
-
dispatch(getComputeNodes({path, ...params}));
|
89
|
+
// If there is no path, it's cluster Nodes tab
|
90
|
+
if (path && !useNodesEndpoint) {
|
91
|
+
dispatch(getComputeNodes({path}));
|
113
92
|
} else {
|
114
|
-
dispatch(getNodes({
|
93
|
+
dispatch(getNodes({path}));
|
115
94
|
}
|
116
95
|
},
|
117
|
-
[dispatch, path,
|
96
|
+
[dispatch, path, useNodesEndpoint],
|
118
97
|
);
|
119
98
|
|
120
99
|
useAutofetcher(fetchNodes, [fetchNodes], isClusterNodes ? true : autorefresh);
|
@@ -0,0 +1,146 @@
|
|
1
|
+
import {useCallback, useMemo, useState} from 'react';
|
2
|
+
import cn from 'bem-cn-lite';
|
3
|
+
|
4
|
+
import type {AdditionalNodesProps} from '../../types/additionalProps';
|
5
|
+
import type {ProblemFilterValue} from '../../store/reducers/settings/types';
|
6
|
+
import type {NodesPreparedEntity} from '../../store/reducers/nodes/types';
|
7
|
+
import {ProblemFilterValues} from '../../store/reducers/settings/settings';
|
8
|
+
import {
|
9
|
+
NodesSortValue,
|
10
|
+
NodesUptimeFilterValues,
|
11
|
+
getProblemParamValue,
|
12
|
+
getUptimeParamValue,
|
13
|
+
isSortableNodesProperty,
|
14
|
+
isUnavailableNode,
|
15
|
+
} from '../../utils/nodes';
|
16
|
+
|
17
|
+
import {Search} from '../../components/Search';
|
18
|
+
import {ProblemFilter} from '../../components/ProblemFilter';
|
19
|
+
import {UptimeFilter} from '../../components/UptimeFIlter';
|
20
|
+
import {EntitiesCount} from '../../components/EntitiesCount';
|
21
|
+
import {AccessDenied} from '../../components/Errors/403';
|
22
|
+
import {ResponseError} from '../../components/Errors/ResponseError';
|
23
|
+
import {Illustration} from '../../components/Illustration';
|
24
|
+
import {
|
25
|
+
type FetchData,
|
26
|
+
type RenderControls,
|
27
|
+
type RenderErrorMessage,
|
28
|
+
VirtualTable,
|
29
|
+
GetRowClassName,
|
30
|
+
} from '../../components/VirtualTable';
|
31
|
+
|
32
|
+
import {getNodesColumns} from './getNodesColumns';
|
33
|
+
import {getNodes} from './getNodes';
|
34
|
+
import i18n from './i18n';
|
35
|
+
|
36
|
+
import './Nodes.scss';
|
37
|
+
|
38
|
+
const b = cn('ydb-nodes');
|
39
|
+
|
40
|
+
interface NodesProps {
|
41
|
+
parentContainer?: Element | null;
|
42
|
+
additionalNodesProps?: AdditionalNodesProps;
|
43
|
+
}
|
44
|
+
|
45
|
+
export const VirtualNodes = ({parentContainer, additionalNodesProps}: NodesProps) => {
|
46
|
+
const [searchValue, setSearchValue] = useState('');
|
47
|
+
const [problemFilter, setProblemFilter] = useState<ProblemFilterValue>(ProblemFilterValues.ALL);
|
48
|
+
const [uptimeFilter, setUptimeFilter] = useState<NodesUptimeFilterValues>(
|
49
|
+
NodesUptimeFilterValues.All,
|
50
|
+
);
|
51
|
+
|
52
|
+
const filters = useMemo(() => {
|
53
|
+
return [searchValue, problemFilter, uptimeFilter];
|
54
|
+
}, [searchValue, problemFilter, uptimeFilter]);
|
55
|
+
|
56
|
+
const fetchData = useCallback<FetchData<NodesPreparedEntity>>(
|
57
|
+
async (limit, offset, {sortOrder, columnId} = {}) => {
|
58
|
+
return await getNodes({
|
59
|
+
limit,
|
60
|
+
offset,
|
61
|
+
filter: searchValue,
|
62
|
+
problems_only: getProblemParamValue(problemFilter),
|
63
|
+
uptime: getUptimeParamValue(uptimeFilter),
|
64
|
+
sortOrder,
|
65
|
+
sortValue: columnId as NodesSortValue,
|
66
|
+
});
|
67
|
+
},
|
68
|
+
[problemFilter, searchValue, uptimeFilter],
|
69
|
+
);
|
70
|
+
|
71
|
+
const getRowClassName: GetRowClassName<NodesPreparedEntity> = (row) => {
|
72
|
+
return b('node', {unavailable: isUnavailableNode(row)});
|
73
|
+
};
|
74
|
+
|
75
|
+
const handleSearchQueryChange = (value: string) => {
|
76
|
+
setSearchValue(value);
|
77
|
+
};
|
78
|
+
const handleProblemFilterChange = (value: string) => {
|
79
|
+
setProblemFilter(value as ProblemFilterValue);
|
80
|
+
};
|
81
|
+
const handleUptimeFilterChange = (value: string) => {
|
82
|
+
setUptimeFilter(value as NodesUptimeFilterValues);
|
83
|
+
};
|
84
|
+
|
85
|
+
const renderControls: RenderControls = ({totalEntities, foundEntities, inited}) => {
|
86
|
+
return (
|
87
|
+
<>
|
88
|
+
<Search
|
89
|
+
onChange={handleSearchQueryChange}
|
90
|
+
placeholder="Host name"
|
91
|
+
className={b('search')}
|
92
|
+
value={searchValue}
|
93
|
+
/>
|
94
|
+
<ProblemFilter value={problemFilter} onChange={handleProblemFilterChange} />
|
95
|
+
<UptimeFilter value={uptimeFilter} onChange={handleUptimeFilterChange} />
|
96
|
+
<EntitiesCount
|
97
|
+
total={totalEntities}
|
98
|
+
current={foundEntities}
|
99
|
+
label={'Nodes'}
|
100
|
+
loading={!inited}
|
101
|
+
/>
|
102
|
+
</>
|
103
|
+
);
|
104
|
+
};
|
105
|
+
|
106
|
+
const renderEmptyDataMessage = () => {
|
107
|
+
if (
|
108
|
+
problemFilter !== ProblemFilterValues.ALL ||
|
109
|
+
uptimeFilter !== NodesUptimeFilterValues.All
|
110
|
+
) {
|
111
|
+
return <Illustration name="thumbsUp" width="200" />;
|
112
|
+
}
|
113
|
+
|
114
|
+
return i18n('empty.default');
|
115
|
+
};
|
116
|
+
|
117
|
+
const renderErrorMessage: RenderErrorMessage = (error) => {
|
118
|
+
if (error && error.status === 403) {
|
119
|
+
return <AccessDenied />;
|
120
|
+
}
|
121
|
+
|
122
|
+
return <ResponseError error={error} />;
|
123
|
+
};
|
124
|
+
|
125
|
+
const rawColumns = getNodesColumns({
|
126
|
+
getNodeRef: additionalNodesProps?.getNodeRef,
|
127
|
+
});
|
128
|
+
|
129
|
+
const columns = rawColumns.map((column) => {
|
130
|
+
return {...column, sortable: isSortableNodesProperty(column.name)};
|
131
|
+
});
|
132
|
+
|
133
|
+
return (
|
134
|
+
<VirtualTable
|
135
|
+
parentContainer={parentContainer}
|
136
|
+
columns={columns}
|
137
|
+
fetchData={fetchData}
|
138
|
+
limit={50}
|
139
|
+
renderControls={renderControls}
|
140
|
+
renderErrorMessage={renderErrorMessage}
|
141
|
+
renderEmptyDataMessage={renderEmptyDataMessage}
|
142
|
+
dependencyArray={filters}
|
143
|
+
getRowClassName={getRowClassName}
|
144
|
+
/>
|
145
|
+
);
|
146
|
+
};
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import type {NodesApiRequestParams} from '../../store/reducers/nodes/types';
|
2
|
+
import {prepareNodesData} from '../../store/reducers/nodes/utils';
|
3
|
+
|
4
|
+
const getConcurrentId = (limit?: number, offset?: number) => {
|
5
|
+
return `getNodes|offset${offset}|limit${limit}`;
|
6
|
+
};
|
7
|
+
|
8
|
+
export const getNodes = async ({
|
9
|
+
type = 'any',
|
10
|
+
storage = false,
|
11
|
+
limit,
|
12
|
+
offset,
|
13
|
+
...params
|
14
|
+
}: NodesApiRequestParams) => {
|
15
|
+
const response = await window.api.getNodes(
|
16
|
+
{type, storage, limit, offset, ...params},
|
17
|
+
{concurrentId: getConcurrentId(limit, offset)},
|
18
|
+
);
|
19
|
+
const preparedResponse = prepareNodesData(response);
|
20
|
+
|
21
|
+
return {
|
22
|
+
data: preparedResponse.Nodes || [],
|
23
|
+
found: preparedResponse.FoundNodes || 0,
|
24
|
+
total: preparedResponse.TotalNodes || 0,
|
25
|
+
};
|
26
|
+
};
|