ydb-embedded-ui 4.22.0 → 4.23.0
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +13 -0
- package/dist/components/ProgressViewer/ProgressViewer.tsx +1 -1
- package/dist/containers/Cluster/Cluster.tsx +2 -0
- package/dist/containers/Cluster/ClusterInfo/ClusterInfo.scss +14 -5
- package/dist/containers/Cluster/ClusterInfo/ClusterInfo.tsx +104 -24
- package/dist/containers/Cluster/ClusterInfoSkeleton/ClusterInfoSkeleton.tsx +1 -1
- package/dist/containers/Cluster/i18n/en.json +16 -0
- package/dist/containers/Cluster/i18n/index.ts +11 -0
- package/dist/containers/Cluster/i18n/ru.json +16 -0
- package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByCpu.tsx +18 -3
- package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByLoad.tsx +18 -3
- package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx +17 -1
- package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx +20 -1
- package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TopNodesByMemory.tsx +18 -3
- package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantOverviewTableLayout.tsx +2 -1
- package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopGroups.tsx +19 -2
- package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx +8 -1
- package/dist/containers/Tenant/Diagnostics/TenantOverview/getSectionTitle.tsx +28 -0
- package/dist/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json +17 -1
- package/dist/containers/Tenant/Diagnostics/TenantOverview/i18n/ru.json +17 -1
- package/dist/containers/Tenant/Schema/SchemaViewer/SchemaViewer.tsx +19 -11
- package/dist/store/reducers/cluster/__test__/parseGroupsStatsQueryResponse.test.ts +121 -0
- package/dist/store/reducers/cluster/cluster.ts +46 -2
- package/dist/store/reducers/cluster/types.ts +29 -4
- package/dist/store/reducers/cluster/utils.ts +88 -0
- package/dist/types/api/cluster.ts +3 -0
- package/dist/utils/hooks/index.ts +1 -0
- package/dist/utils/hooks/useSearchQuery.ts +9 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [4.23.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v4.22.0...v4.23.0) (2023-12-06)
|
4
|
+
|
5
|
+
|
6
|
+
### Features
|
7
|
+
|
8
|
+
* **ClusterInfo:** display groups stats ([#598](https://github.com/ydb-platform/ydb-embedded-ui/issues/598)) ([c31d048](https://github.com/ydb-platform/ydb-embedded-ui/commit/c31d0480a1b91cf01a660fd1d9726c6708f7c252))
|
9
|
+
* **TenantOverview:** add links to sections titles ([#599](https://github.com/ydb-platform/ydb-embedded-ui/issues/599)) ([30401fa](https://github.com/ydb-platform/ydb-embedded-ui/commit/30401fa354d90943bc4af4ddbf65466ce10381f9))
|
10
|
+
|
11
|
+
|
12
|
+
### Bug Fixes
|
13
|
+
|
14
|
+
* **Schema:** display keys in right order ([#596](https://github.com/ydb-platform/ydb-embedded-ui/issues/596)) ([c99b7e2](https://github.com/ydb-platform/ydb-embedded-ui/commit/c99b7e2e97acffc1cab450dfbf758c38b8b6e4d5))
|
15
|
+
|
3
16
|
## [4.22.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v4.21.1...v4.22.0) (2023-11-27)
|
4
17
|
|
5
18
|
|
@@ -44,7 +44,7 @@ Props description:
|
|
44
44
|
interface ProgressViewerProps {
|
45
45
|
value?: number | string;
|
46
46
|
capacity?: number | string;
|
47
|
-
formatValues?: (value?: number, capacity?: number) => (string | undefined)[];
|
47
|
+
formatValues?: (value?: number, capacity?: number) => (string | number | undefined)[];
|
48
48
|
percents?: boolean;
|
49
49
|
className?: string;
|
50
50
|
size?: ProgressViewerSize;
|
@@ -68,6 +68,7 @@ function Cluster({
|
|
68
68
|
loading: clusterLoading,
|
69
69
|
wasLoaded: clusterWasLoaded,
|
70
70
|
error: clusterError,
|
71
|
+
groupsStats,
|
71
72
|
} = useTypedSelector((state) => state.cluster);
|
72
73
|
const {
|
73
74
|
nodes,
|
@@ -135,6 +136,7 @@ function Cluster({
|
|
135
136
|
<div className={b()} ref={container}>
|
136
137
|
<ClusterInfo
|
137
138
|
cluster={cluster}
|
139
|
+
groupsStats={groupsStats}
|
138
140
|
versionsValues={versionsValues}
|
139
141
|
loading={infoLoading}
|
140
142
|
error={clusterError}
|
@@ -49,10 +49,6 @@
|
|
49
49
|
}
|
50
50
|
}
|
51
51
|
|
52
|
-
&__metric-field {
|
53
|
-
margin-top: -8px;
|
54
|
-
}
|
55
|
-
|
56
52
|
&__system-tablets {
|
57
53
|
display: flex;
|
58
54
|
flex-wrap: wrap;
|
@@ -83,7 +79,6 @@
|
|
83
79
|
margin-left: 15px;
|
84
80
|
padding: 0 !important;
|
85
81
|
}
|
86
|
-
|
87
82
|
&__links {
|
88
83
|
display: flex;
|
89
84
|
flex-flow: row wrap;
|
@@ -91,6 +86,20 @@
|
|
91
86
|
gap: 12px;
|
92
87
|
}
|
93
88
|
|
89
|
+
&__storage-groups-stats {
|
90
|
+
display: flex;
|
91
|
+
flex-direction: column;
|
92
|
+
gap: 11px;
|
93
|
+
}
|
94
|
+
|
95
|
+
&__groups-stats-bar {
|
96
|
+
cursor: pointer;
|
97
|
+
}
|
98
|
+
|
99
|
+
&__groups-stats-popup-content {
|
100
|
+
padding: 12px;
|
101
|
+
}
|
102
|
+
|
94
103
|
&__clipboard-button {
|
95
104
|
display: flex;
|
96
105
|
align-items: center;
|
@@ -10,6 +10,7 @@ import {Tablet} from '../../../components/Tablet';
|
|
10
10
|
import {ResponseError} from '../../../components/Errors/ResponseError';
|
11
11
|
import {ExternalLinkWithIcon} from '../../../components/ExternalLinkWithIcon/ExternalLinkWithIcon';
|
12
12
|
import {IconWrapper as Icon} from '../../../components/Icon/Icon';
|
13
|
+
import {ContentWithPopup} from '../../../components/ContentWithPopup/ContentWithPopup';
|
13
14
|
|
14
15
|
import type {IResponseError} from '../../../types/api/error';
|
15
16
|
import type {AdditionalClusterProps, ClusterLink} from '../../../types/additionalProps';
|
@@ -18,14 +19,21 @@ import type {TClusterInfo} from '../../../types/api/cluster';
|
|
18
19
|
import {backend, customBackend} from '../../../store';
|
19
20
|
import {formatStorageValues} from '../../../utils/dataFormatters/dataFormatters';
|
20
21
|
import {useSetting, useTypedSelector} from '../../../utils/hooks';
|
22
|
+
import {formatBytes, getSizeWithSignificantDigits} from '../../../utils/bytesParsers';
|
21
23
|
import {
|
22
24
|
CLUSTER_DEFAULT_TITLE,
|
23
25
|
CLUSTER_INFO_HIDDEN_KEY,
|
24
26
|
DEVELOPER_UI_TITLE,
|
25
27
|
} from '../../../utils/constants';
|
28
|
+
import type {
|
29
|
+
ClusterGroupsStats,
|
30
|
+
DiskErasureGroupsStats,
|
31
|
+
DiskGroupsStats,
|
32
|
+
} from '../../../store/reducers/cluster/types';
|
26
33
|
|
27
34
|
import {VersionsBar} from '../VersionsBar/VersionsBar';
|
28
35
|
import {ClusterInfoSkeleton} from '../ClusterInfoSkeleton/ClusterInfoSkeleton';
|
36
|
+
import i18n from '../i18n';
|
29
37
|
|
30
38
|
import {compareTablets} from './utils';
|
31
39
|
|
@@ -33,9 +41,85 @@ import './ClusterInfo.scss';
|
|
33
41
|
|
34
42
|
const b = block('cluster-info');
|
35
43
|
|
44
|
+
interface GroupsStatsPopupContentProps {
|
45
|
+
stats: DiskErasureGroupsStats;
|
46
|
+
}
|
47
|
+
|
48
|
+
const GroupsStatsPopupContent = ({stats}: GroupsStatsPopupContentProps) => {
|
49
|
+
const {diskType, erasure, allocatedSize, availableSize} = stats;
|
50
|
+
|
51
|
+
const sizeToConvert = getSizeWithSignificantDigits(Math.max(allocatedSize, availableSize), 2);
|
52
|
+
|
53
|
+
const convertedAllocatedSize = formatBytes({value: allocatedSize, size: sizeToConvert});
|
54
|
+
const convertedAvailableSize = formatBytes({value: availableSize, size: sizeToConvert});
|
55
|
+
|
56
|
+
const usage = Math.round((allocatedSize / (allocatedSize + availableSize)) * 100);
|
57
|
+
|
58
|
+
const info = [
|
59
|
+
{
|
60
|
+
label: i18n('disk-type'),
|
61
|
+
value: diskType,
|
62
|
+
},
|
63
|
+
{
|
64
|
+
label: i18n('erasure'),
|
65
|
+
value: erasure,
|
66
|
+
},
|
67
|
+
{
|
68
|
+
label: i18n('allocated'),
|
69
|
+
value: convertedAllocatedSize,
|
70
|
+
},
|
71
|
+
{
|
72
|
+
label: i18n('available'),
|
73
|
+
value: convertedAvailableSize,
|
74
|
+
},
|
75
|
+
{
|
76
|
+
label: i18n('usage'),
|
77
|
+
value: usage + '%',
|
78
|
+
},
|
79
|
+
];
|
80
|
+
|
81
|
+
return (
|
82
|
+
<InfoViewer dots={true} info={info} className={b('groups-stats-popup-content')} size="s" />
|
83
|
+
);
|
84
|
+
};
|
85
|
+
|
86
|
+
interface DiskGroupsStatsProps {
|
87
|
+
stats: DiskGroupsStats;
|
88
|
+
}
|
89
|
+
|
90
|
+
const DiskGroupsStatsBars = ({stats}: DiskGroupsStatsProps) => {
|
91
|
+
return (
|
92
|
+
<div className={b('storage-groups-stats')}>
|
93
|
+
{Object.values(stats).map((erasureStats) => (
|
94
|
+
<ContentWithPopup
|
95
|
+
placement={['right']}
|
96
|
+
key={erasureStats.erasure}
|
97
|
+
content={<GroupsStatsPopupContent stats={erasureStats} />}
|
98
|
+
>
|
99
|
+
<ProgressViewer
|
100
|
+
className={b('groups-stats-bar')}
|
101
|
+
value={erasureStats.createdGroups}
|
102
|
+
capacity={erasureStats.totalGroups}
|
103
|
+
/>
|
104
|
+
</ContentWithPopup>
|
105
|
+
))}
|
106
|
+
</div>
|
107
|
+
);
|
108
|
+
};
|
109
|
+
|
110
|
+
const getGroupsStatsFields = (groupsStats: ClusterGroupsStats) => {
|
111
|
+
return Object.keys(groupsStats).map((diskType) => {
|
112
|
+
return {
|
113
|
+
label: i18n('storage-groups', {diskType}),
|
114
|
+
value: <DiskGroupsStatsBars stats={groupsStats[diskType]} />,
|
115
|
+
};
|
116
|
+
});
|
117
|
+
};
|
118
|
+
|
36
119
|
const getInfo = (
|
37
120
|
cluster: TClusterInfo,
|
38
121
|
versionsValues: VersionValue[],
|
122
|
+
groupsStats: ClusterGroupsStats,
|
39
123
|
additionalInfo: InfoViewerItem[],
|
40
124
|
links: ClusterLink[],
|
41
125
|
) => {
|
@@ -43,14 +127,14 @@ const getInfo = (
|
|
43
127
|
|
44
128
|
if (cluster.DataCenters) {
|
45
129
|
info.push({
|
46
|
-
label: '
|
130
|
+
label: i18n('dc'),
|
47
131
|
value: <Tags tags={cluster.DataCenters} />,
|
48
132
|
});
|
49
133
|
}
|
50
134
|
|
51
135
|
if (cluster.SystemTablets) {
|
52
136
|
info.push({
|
53
|
-
label: '
|
137
|
+
label: i18n('tablets'),
|
54
138
|
value: (
|
55
139
|
<div className={b('system-tablets')}>
|
56
140
|
{cluster.SystemTablets.sort(compareTablets).map((tablet, tabletIndex) => (
|
@@ -63,46 +147,40 @@ const getInfo = (
|
|
63
147
|
|
64
148
|
if (cluster.Tenants) {
|
65
149
|
info.push({
|
66
|
-
label: '
|
150
|
+
label: i18n('databases'),
|
67
151
|
value: cluster.Tenants,
|
68
152
|
});
|
69
153
|
}
|
70
154
|
|
71
155
|
info.push(
|
72
156
|
{
|
73
|
-
label: '
|
74
|
-
value:
|
75
|
-
<ProgressViewer
|
76
|
-
className={b('metric-field')}
|
77
|
-
value={cluster?.NodesAlive}
|
78
|
-
capacity={cluster?.NodesTotal}
|
79
|
-
/>
|
80
|
-
),
|
157
|
+
label: i18n('nodes'),
|
158
|
+
value: <ProgressViewer value={cluster?.NodesAlive} capacity={cluster?.NodesTotal} />,
|
81
159
|
},
|
82
160
|
{
|
83
|
-
label: '
|
84
|
-
value:
|
85
|
-
<ProgressViewer
|
86
|
-
className={b('metric-field')}
|
87
|
-
value={cluster?.LoadAverage}
|
88
|
-
capacity={cluster?.NumberOfCpus}
|
89
|
-
/>
|
90
|
-
),
|
161
|
+
label: i18n('load'),
|
162
|
+
value: <ProgressViewer value={cluster?.LoadAverage} capacity={cluster?.NumberOfCpus} />,
|
91
163
|
},
|
92
164
|
{
|
93
|
-
label: '
|
165
|
+
label: i18n('storage-size'),
|
94
166
|
value: (
|
95
167
|
<ProgressViewer
|
96
|
-
className={b('metric-field')}
|
97
168
|
value={cluster?.StorageUsed}
|
98
169
|
capacity={cluster?.StorageTotal}
|
99
170
|
formatValues={formatStorageValues}
|
100
171
|
/>
|
101
172
|
),
|
102
173
|
},
|
174
|
+
);
|
175
|
+
|
176
|
+
if (Object.keys(groupsStats).length) {
|
177
|
+
info.push(...getGroupsStatsFields(groupsStats));
|
178
|
+
}
|
179
|
+
|
180
|
+
info.push(
|
103
181
|
...additionalInfo,
|
104
182
|
{
|
105
|
-
label: '
|
183
|
+
label: i18n('links'),
|
106
184
|
value: (
|
107
185
|
<div className={b('links')}>
|
108
186
|
{links.map(({title, url}) => (
|
@@ -112,7 +190,7 @@ const getInfo = (
|
|
112
190
|
),
|
113
191
|
},
|
114
192
|
{
|
115
|
-
label: '
|
193
|
+
label: i18n('versions'),
|
116
194
|
value: <VersionsBar versionsValues={versionsValues} />,
|
117
195
|
},
|
118
196
|
);
|
@@ -123,6 +201,7 @@ const getInfo = (
|
|
123
201
|
interface ClusterInfoProps {
|
124
202
|
cluster?: TClusterInfo;
|
125
203
|
versionsValues?: VersionValue[];
|
204
|
+
groupsStats?: ClusterGroupsStats;
|
126
205
|
loading?: boolean;
|
127
206
|
error?: IResponseError;
|
128
207
|
additionalClusterProps?: AdditionalClusterProps;
|
@@ -131,6 +210,7 @@ interface ClusterInfoProps {
|
|
131
210
|
export const ClusterInfo = ({
|
132
211
|
cluster = {},
|
133
212
|
versionsValues = [],
|
213
|
+
groupsStats = {},
|
134
214
|
loading,
|
135
215
|
error,
|
136
216
|
additionalClusterProps = {},
|
@@ -151,7 +231,7 @@ export const ClusterInfo = ({
|
|
151
231
|
|
152
232
|
const {info = [], links = []} = additionalClusterProps;
|
153
233
|
|
154
|
-
const clusterInfo = getInfo(cluster, versionsValues, info, [
|
234
|
+
const clusterInfo = getInfo(cluster, versionsValues, groupsStats, info, [
|
155
235
|
{title: DEVELOPER_UI_TITLE, url: internalLink},
|
156
236
|
...links,
|
157
237
|
]);
|
@@ -18,7 +18,7 @@ interface ClusterInfoSkeletonProps {
|
|
18
18
|
rows?: number;
|
19
19
|
}
|
20
20
|
|
21
|
-
export const ClusterInfoSkeleton = ({rows =
|
21
|
+
export const ClusterInfoSkeleton = ({rows = 8, className}: ClusterInfoSkeletonProps) => (
|
22
22
|
<div className={b(null, className)}>
|
23
23
|
{[...new Array(rows)].map((_, index) => (
|
24
24
|
<div className={b('row')} key={`skeleton-row-${index}`}>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
{
|
2
|
+
"disk-type": "Disk Type",
|
3
|
+
"erasure": "Erasure",
|
4
|
+
"allocated": "Allocated",
|
5
|
+
"available": "Available",
|
6
|
+
"usage": "Usage",
|
7
|
+
"dc": "DC",
|
8
|
+
"tablets": "Tablets",
|
9
|
+
"databases": "Databases",
|
10
|
+
"nodes": "Nodes",
|
11
|
+
"load": "Load",
|
12
|
+
"storage-size": "Storage size",
|
13
|
+
"storage-groups": "Storage groups, {{diskType}}",
|
14
|
+
"links": "Links",
|
15
|
+
"versions": "Versions"
|
16
|
+
}
|
@@ -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-cluster';
|
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,16 @@
|
|
1
|
+
{
|
2
|
+
"disk-type": "Тип диска",
|
3
|
+
"erasure": "Режим",
|
4
|
+
"allocated": "Использовано",
|
5
|
+
"available": "Доступно",
|
6
|
+
"usage": "Потребление",
|
7
|
+
"dc": "ДЦ",
|
8
|
+
"tablets": "Таблетки",
|
9
|
+
"databases": "Базы данных",
|
10
|
+
"nodes": "Узлы",
|
11
|
+
"load": "Нагрузка",
|
12
|
+
"storage-size": "Размер хранилища",
|
13
|
+
"storage-groups": "Группы хранения, {{diskType}}",
|
14
|
+
"links": "Ссылки",
|
15
|
+
"versions": "Версии"
|
16
|
+
}
|
@@ -1,16 +1,20 @@
|
|
1
1
|
import {useDispatch} from 'react-redux';
|
2
2
|
import {useCallback} from 'react';
|
3
3
|
|
4
|
-
import {useAutofetcher, useTypedSelector} from '../../../../../utils/hooks';
|
4
|
+
import {useAutofetcher, useSearchQuery, useTypedSelector} from '../../../../../utils/hooks';
|
5
5
|
import {
|
6
6
|
getTopNodesByCpu,
|
7
7
|
selectTopNodesByCpu,
|
8
8
|
setDataWasNotLoaded,
|
9
9
|
} from '../../../../../store/reducers/tenantOverview/topNodesByCpu/topNodesByCpu';
|
10
|
+
import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants';
|
10
11
|
import type {AdditionalNodesProps} from '../../../../../types/additionalProps';
|
11
12
|
import {getTopNodesByCpuColumns} from '../../../../Nodes/getNodesColumns';
|
12
|
-
import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout';
|
13
13
|
|
14
|
+
import {TenantTabsGroups, getTenantPath} from '../../../TenantPages';
|
15
|
+
|
16
|
+
import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout';
|
17
|
+
import {getSectionTitle} from '../getSectionTitle';
|
14
18
|
import i18n from '../i18n';
|
15
19
|
|
16
20
|
interface TopNodesByCpuProps {
|
@@ -21,6 +25,8 @@ interface TopNodesByCpuProps {
|
|
21
25
|
export function TopNodesByCpu({path, additionalNodesProps}: TopNodesByCpuProps) {
|
22
26
|
const dispatch = useDispatch();
|
23
27
|
|
28
|
+
const query = useSearchQuery();
|
29
|
+
|
24
30
|
const {wasLoaded, loading, error} = useTypedSelector((state) => state.topNodesByCpu);
|
25
31
|
const {autorefresh} = useTypedSelector((state) => state.schema);
|
26
32
|
const topNodes = useTypedSelector(selectTopNodesByCpu);
|
@@ -39,11 +45,20 @@ export function TopNodesByCpu({path, additionalNodesProps}: TopNodesByCpuProps)
|
|
39
45
|
|
40
46
|
useAutofetcher(fetchNodes, [fetchNodes], autorefresh);
|
41
47
|
|
48
|
+
const title = getSectionTitle({
|
49
|
+
entity: i18n('nodes'),
|
50
|
+
postfix: i18n('by-pools-usage'),
|
51
|
+
link: getTenantPath({
|
52
|
+
...query,
|
53
|
+
[TenantTabsGroups.diagnosticsTab]: TENANT_DIAGNOSTICS_TABS_IDS.nodes,
|
54
|
+
}),
|
55
|
+
});
|
56
|
+
|
42
57
|
return (
|
43
58
|
<TenantOverviewTableLayout
|
44
59
|
data={topNodes || []}
|
45
60
|
columns={columns}
|
46
|
-
title=
|
61
|
+
title={title}
|
47
62
|
loading={loading}
|
48
63
|
wasLoaded={wasLoaded}
|
49
64
|
error={error}
|
@@ -1,16 +1,20 @@
|
|
1
1
|
import {useDispatch} from 'react-redux';
|
2
2
|
import {useCallback} from 'react';
|
3
3
|
|
4
|
-
import {useAutofetcher, useTypedSelector} from '../../../../../utils/hooks';
|
4
|
+
import {useAutofetcher, useSearchQuery, useTypedSelector} from '../../../../../utils/hooks';
|
5
5
|
import {
|
6
6
|
getTopNodesByLoad,
|
7
7
|
selectTopNodesByLoad,
|
8
8
|
setDataWasNotLoaded,
|
9
9
|
} from '../../../../../store/reducers/tenantOverview/topNodesByLoad/topNodesByLoad';
|
10
|
+
import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants';
|
10
11
|
import type {AdditionalNodesProps} from '../../../../../types/additionalProps';
|
11
12
|
import {getTopNodesByLoadColumns} from '../../../../Nodes/getNodesColumns';
|
12
|
-
import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout';
|
13
13
|
|
14
|
+
import {TenantTabsGroups, getTenantPath} from '../../../TenantPages';
|
15
|
+
|
16
|
+
import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout';
|
17
|
+
import {getSectionTitle} from '../getSectionTitle';
|
14
18
|
import i18n from '../i18n';
|
15
19
|
|
16
20
|
interface TopNodesByLoadProps {
|
@@ -21,6 +25,8 @@ interface TopNodesByLoadProps {
|
|
21
25
|
export function TopNodesByLoad({path, additionalNodesProps}: TopNodesByLoadProps) {
|
22
26
|
const dispatch = useDispatch();
|
23
27
|
|
28
|
+
const query = useSearchQuery();
|
29
|
+
|
24
30
|
const {wasLoaded, loading, error} = useTypedSelector((state) => state.topNodesByLoad);
|
25
31
|
const {autorefresh} = useTypedSelector((state) => state.schema);
|
26
32
|
const topNodes = useTypedSelector(selectTopNodesByLoad);
|
@@ -39,11 +45,20 @@ export function TopNodesByLoad({path, additionalNodesProps}: TopNodesByLoadProps
|
|
39
45
|
|
40
46
|
useAutofetcher(fetchNodes, [fetchNodes], autorefresh);
|
41
47
|
|
48
|
+
const title = getSectionTitle({
|
49
|
+
entity: i18n('nodes'),
|
50
|
+
postfix: i18n('by-load'),
|
51
|
+
link: getTenantPath({
|
52
|
+
...query,
|
53
|
+
[TenantTabsGroups.diagnosticsTab]: TENANT_DIAGNOSTICS_TABS_IDS.nodes,
|
54
|
+
}),
|
55
|
+
});
|
56
|
+
|
42
57
|
return (
|
43
58
|
<TenantOverviewTableLayout
|
44
59
|
data={topNodes || []}
|
45
60
|
columns={columns}
|
46
|
-
title=
|
61
|
+
title={title}
|
47
62
|
loading={loading}
|
48
63
|
wasLoaded={wasLoaded}
|
49
64
|
error={error}
|
@@ -3,6 +3,7 @@ import {useHistory, useLocation} from 'react-router';
|
|
3
3
|
import {useCallback} from 'react';
|
4
4
|
|
5
5
|
import {
|
6
|
+
TENANT_DIAGNOSTICS_TABS_IDS,
|
6
7
|
TENANT_PAGE,
|
7
8
|
TENANT_PAGES_IDS,
|
8
9
|
TENANT_QUERY_TABS_ID,
|
@@ -14,9 +15,13 @@ import {
|
|
14
15
|
import {changeUserInput} from '../../../../../store/reducers/executeQuery';
|
15
16
|
import {useAutofetcher, useTypedSelector} from '../../../../../utils/hooks';
|
16
17
|
import {parseQuery} from '../../../../../routes';
|
18
|
+
|
17
19
|
import {TenantTabsGroups, getTenantPath} from '../../../TenantPages';
|
18
20
|
import {getTenantOverviewTopQueriesColumns} from '../../TopQueries/getTopQueriesColumns';
|
21
|
+
|
19
22
|
import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout';
|
23
|
+
import {getSectionTitle} from '../getSectionTitle';
|
24
|
+
import i18n from '../i18n';
|
20
25
|
|
21
26
|
interface TopQueriesProps {
|
22
27
|
path: string;
|
@@ -27,6 +32,8 @@ export function TopQueries({path}: TopQueriesProps) {
|
|
27
32
|
const location = useLocation();
|
28
33
|
const history = useHistory();
|
29
34
|
|
35
|
+
const query = parseQuery(location);
|
36
|
+
|
30
37
|
const {autorefresh} = useTypedSelector((state) => state.schema);
|
31
38
|
|
32
39
|
const {
|
@@ -68,12 +75,21 @@ export function TopQueries({path}: TopQueriesProps) {
|
|
68
75
|
[dispatch, history, location],
|
69
76
|
);
|
70
77
|
|
78
|
+
const title = getSectionTitle({
|
79
|
+
entity: i18n('queries'),
|
80
|
+
postfix: i18n('by-cpu-time'),
|
81
|
+
link: getTenantPath({
|
82
|
+
...query,
|
83
|
+
[TenantTabsGroups.diagnosticsTab]: TENANT_DIAGNOSTICS_TABS_IDS.topQueries,
|
84
|
+
}),
|
85
|
+
});
|
86
|
+
|
71
87
|
return (
|
72
88
|
<TenantOverviewTableLayout
|
73
89
|
data={data || []}
|
74
90
|
columns={columns}
|
75
91
|
onRowClick={handleRowClick}
|
76
|
-
title=
|
92
|
+
title={title}
|
77
93
|
loading={loading}
|
78
94
|
wasLoaded={wasLoaded}
|
79
95
|
error={error}
|
@@ -2,12 +2,20 @@ import {useDispatch} from 'react-redux';
|
|
2
2
|
import {useLocation} from 'react-router';
|
3
3
|
|
4
4
|
import {useAutofetcher, useTypedSelector} from '../../../../../utils/hooks';
|
5
|
+
import {parseQuery} from '../../../../../routes';
|
6
|
+
|
5
7
|
import {
|
6
8
|
sendTenantOverviewTopShardsQuery,
|
7
9
|
setDataWasNotLoaded,
|
8
10
|
} from '../../../../../store/reducers/tenantOverview/topShards/tenantOverviewTopShards';
|
11
|
+
import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants';
|
9
12
|
import {getTopShardsColumns} from '../../TopShards/getTopShardsColumns';
|
13
|
+
|
14
|
+
import {TenantTabsGroups, getTenantPath} from '../../../TenantPages';
|
15
|
+
|
10
16
|
import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout';
|
17
|
+
import {getSectionTitle} from '../getSectionTitle';
|
18
|
+
import i18n from '../i18n';
|
11
19
|
|
12
20
|
interface TopShardsProps {
|
13
21
|
path: string;
|
@@ -17,6 +25,8 @@ export const TopShards = ({path}: TopShardsProps) => {
|
|
17
25
|
const dispatch = useDispatch();
|
18
26
|
const location = useLocation();
|
19
27
|
|
28
|
+
const query = parseQuery(location);
|
29
|
+
|
20
30
|
const {autorefresh, currentSchemaPath} = useTypedSelector((state) => state.schema);
|
21
31
|
|
22
32
|
const {
|
@@ -39,11 +49,20 @@ export const TopShards = ({path}: TopShardsProps) => {
|
|
39
49
|
|
40
50
|
const columns = getTopShardsColumns(path, location);
|
41
51
|
|
52
|
+
const title = getSectionTitle({
|
53
|
+
entity: i18n('shards'),
|
54
|
+
postfix: i18n('by-cpu-usage'),
|
55
|
+
link: getTenantPath({
|
56
|
+
...query,
|
57
|
+
[TenantTabsGroups.diagnosticsTab]: TENANT_DIAGNOSTICS_TABS_IDS.topShards,
|
58
|
+
}),
|
59
|
+
});
|
60
|
+
|
42
61
|
return (
|
43
62
|
<TenantOverviewTableLayout
|
44
63
|
data={data || []}
|
45
64
|
columns={columns}
|
46
|
-
title=
|
65
|
+
title={title}
|
47
66
|
loading={loading}
|
48
67
|
wasLoaded={wasLoaded}
|
49
68
|
error={error}
|
@@ -1,16 +1,20 @@
|
|
1
1
|
import {useDispatch} from 'react-redux';
|
2
2
|
import {useCallback} from 'react';
|
3
3
|
|
4
|
-
import {useAutofetcher, useTypedSelector} from '../../../../../utils/hooks';
|
4
|
+
import {useAutofetcher, useTypedSelector, useSearchQuery} from '../../../../../utils/hooks';
|
5
5
|
import {
|
6
6
|
getTopNodesByMemory,
|
7
7
|
selectTopNodesByMemory,
|
8
8
|
setDataWasNotLoaded,
|
9
9
|
} from '../../../../../store/reducers/tenantOverview/topNodesByMemory/topNodesByMemory';
|
10
|
+
import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants';
|
10
11
|
import type {AdditionalNodesProps} from '../../../../../types/additionalProps';
|
11
12
|
import {getTopNodesByMemoryColumns} from '../../../../Nodes/getNodesColumns';
|
12
|
-
import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout';
|
13
13
|
|
14
|
+
import {TenantTabsGroups, getTenantPath} from '../../../TenantPages';
|
15
|
+
|
16
|
+
import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout';
|
17
|
+
import {getSectionTitle} from '../getSectionTitle';
|
14
18
|
import i18n from '../i18n';
|
15
19
|
|
16
20
|
interface TopNodesByMemoryProps {
|
@@ -21,6 +25,8 @@ interface TopNodesByMemoryProps {
|
|
21
25
|
export function TopNodesByMemory({path, additionalNodesProps}: TopNodesByMemoryProps) {
|
22
26
|
const dispatch = useDispatch();
|
23
27
|
|
28
|
+
const query = useSearchQuery();
|
29
|
+
|
24
30
|
const {wasLoaded, loading, error} = useTypedSelector((state) => state.topNodesByMemory);
|
25
31
|
const {autorefresh} = useTypedSelector((state) => state.schema);
|
26
32
|
const topNodes = useTypedSelector(selectTopNodesByMemory);
|
@@ -41,11 +47,20 @@ export function TopNodesByMemory({path, additionalNodesProps}: TopNodesByMemoryP
|
|
41
47
|
|
42
48
|
useAutofetcher(fetchNodes, [fetchNodes], autorefresh);
|
43
49
|
|
50
|
+
const title = getSectionTitle({
|
51
|
+
entity: i18n('nodes'),
|
52
|
+
postfix: i18n('by-memory'),
|
53
|
+
link: getTenantPath({
|
54
|
+
...query,
|
55
|
+
[TenantTabsGroups.diagnosticsTab]: TENANT_DIAGNOSTICS_TABS_IDS.nodes,
|
56
|
+
}),
|
57
|
+
});
|
58
|
+
|
44
59
|
return (
|
45
60
|
<TenantOverviewTableLayout
|
46
61
|
data={topNodes || []}
|
47
62
|
columns={columns}
|
48
|
-
title=
|
63
|
+
title={title}
|
49
64
|
loading={loading}
|
50
65
|
wasLoaded={wasLoaded}
|
51
66
|
error={error}
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import type {ReactNode} from 'react';
|
1
2
|
import cn from 'bem-cn-lite';
|
2
3
|
|
3
4
|
import DataTable from '@gravity-ui/react-data-table';
|
@@ -14,7 +15,7 @@ import {ResponseError} from '../../../../components/Errors/ResponseError';
|
|
14
15
|
const b = cn('tenant-overview');
|
15
16
|
|
16
17
|
interface TenantOverviewTableLayoutProps<T> extends Omit<DataTableProps<T>, 'theme'> {
|
17
|
-
title:
|
18
|
+
title: ReactNode;
|
18
19
|
loading?: boolean;
|
19
20
|
wasLoaded?: boolean;
|
20
21
|
error?: IResponseError;
|
@@ -1,14 +1,20 @@
|
|
1
1
|
import {useCallback} from 'react';
|
2
2
|
import {useDispatch} from 'react-redux';
|
3
3
|
|
4
|
-
import {useAutofetcher, useTypedSelector} from '../../../../../utils/hooks';
|
4
|
+
import {useAutofetcher, useSearchQuery, useTypedSelector} from '../../../../../utils/hooks';
|
5
5
|
import {
|
6
6
|
setDataWasNotLoaded,
|
7
7
|
getTopStorageGroups,
|
8
8
|
selectTopStorageGroups,
|
9
9
|
} from '../../../../../store/reducers/tenantOverview/topStorageGroups/topStorageGroups';
|
10
|
+
import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants';
|
10
11
|
import {getStorageTopGroupsColumns} from '../../../../Storage/StorageGroups/getStorageGroupsColumns';
|
12
|
+
|
13
|
+
import {TenantTabsGroups, getTenantPath} from '../../../TenantPages';
|
14
|
+
|
11
15
|
import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout';
|
16
|
+
import {getSectionTitle} from '../getSectionTitle';
|
17
|
+
import i18n from '../i18n';
|
12
18
|
|
13
19
|
interface TopGroupsProps {
|
14
20
|
tenant?: string;
|
@@ -17,6 +23,8 @@ interface TopGroupsProps {
|
|
17
23
|
export function TopGroups({tenant}: TopGroupsProps) {
|
18
24
|
const dispatch = useDispatch();
|
19
25
|
|
26
|
+
const query = useSearchQuery();
|
27
|
+
|
20
28
|
const {autorefresh} = useTypedSelector((state) => state.schema);
|
21
29
|
const {loading, wasLoaded, error} = useTypedSelector((state) => state.topStorageGroups);
|
22
30
|
const topGroups = useTypedSelector(selectTopStorageGroups);
|
@@ -36,11 +44,20 @@ export function TopGroups({tenant}: TopGroupsProps) {
|
|
36
44
|
|
37
45
|
useAutofetcher(fetchData, [fetchData], autorefresh);
|
38
46
|
|
47
|
+
const title = getSectionTitle({
|
48
|
+
entity: i18n('groups'),
|
49
|
+
postfix: i18n('by-usage'),
|
50
|
+
link: getTenantPath({
|
51
|
+
...query,
|
52
|
+
[TenantTabsGroups.diagnosticsTab]: TENANT_DIAGNOSTICS_TABS_IDS.storage,
|
53
|
+
}),
|
54
|
+
});
|
55
|
+
|
39
56
|
return (
|
40
57
|
<TenantOverviewTableLayout
|
41
58
|
data={topGroups || []}
|
42
59
|
columns={columns}
|
43
|
-
title=
|
60
|
+
title={title}
|
44
61
|
loading={loading}
|
45
62
|
wasLoaded={wasLoaded}
|
46
63
|
error={error}
|
@@ -12,7 +12,10 @@ import type {KeyValueRow} from '../../../../../types/api/query';
|
|
12
12
|
import {formatBytes, getSizeWithSignificantDigits} from '../../../../../utils/bytesParsers';
|
13
13
|
import {LinkToSchemaObject} from '../../../../../components/LinkToSchemaObject/LinkToSchemaObject';
|
14
14
|
import {CellWithPopover} from '../../../../../components/CellWithPopover/CellWithPopover';
|
15
|
+
|
15
16
|
import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout';
|
17
|
+
import {getSectionTitle} from '../getSectionTitle';
|
18
|
+
import i18n from '../i18n';
|
16
19
|
|
17
20
|
import '../TenantOverview.scss';
|
18
21
|
|
@@ -72,12 +75,16 @@ export function TopTables({path}: TopTablesProps) {
|
|
72
75
|
) : null,
|
73
76
|
},
|
74
77
|
];
|
78
|
+
const title = getSectionTitle({
|
79
|
+
entity: i18n('tables'),
|
80
|
+
postfix: i18n('by-size'),
|
81
|
+
});
|
75
82
|
|
76
83
|
return (
|
77
84
|
<TenantOverviewTableLayout
|
78
85
|
data={data || []}
|
79
86
|
columns={columns}
|
80
|
-
title=
|
87
|
+
title={title}
|
81
88
|
loading={loading}
|
82
89
|
wasLoaded={wasLoaded}
|
83
90
|
error={error}
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import {InternalLink} from '../../../../components/InternalLink/InternalLink';
|
2
|
+
|
3
|
+
import i18n from './i18n';
|
4
|
+
|
5
|
+
interface GetSectionTitleParams {
|
6
|
+
entity: string;
|
7
|
+
postfix: string;
|
8
|
+
prefix?: string;
|
9
|
+
link?: string;
|
10
|
+
}
|
11
|
+
|
12
|
+
// Titles are formed by the principle "Top entities by parameter"
|
13
|
+
export const getSectionTitle = ({
|
14
|
+
prefix = i18n('top'),
|
15
|
+
entity,
|
16
|
+
postfix,
|
17
|
+
link,
|
18
|
+
}: GetSectionTitleParams) => {
|
19
|
+
if (link) {
|
20
|
+
return (
|
21
|
+
<>
|
22
|
+
{prefix} <InternalLink to={link}>{entity}</InternalLink> {postfix}
|
23
|
+
</>
|
24
|
+
);
|
25
|
+
}
|
26
|
+
|
27
|
+
return `${prefix} ${entity} ${postfix}`;
|
28
|
+
};
|
@@ -7,5 +7,21 @@
|
|
7
7
|
"title.pools": "Pools",
|
8
8
|
"title.metrics": "Metrics",
|
9
9
|
|
10
|
-
"top-groups.empty-data": "No such groups"
|
10
|
+
"top-groups.empty-data": "No such groups",
|
11
|
+
|
12
|
+
"top": "Top",
|
13
|
+
|
14
|
+
"nodes": "nodes",
|
15
|
+
"shards": "shards",
|
16
|
+
"groups": "groups",
|
17
|
+
"queries": "queries",
|
18
|
+
"tables": "tables",
|
19
|
+
|
20
|
+
"by-pools-usage": "by pools usage",
|
21
|
+
"by-cpu-time": "by cpu time",
|
22
|
+
"by-cpu-usage": "by cpu usage",
|
23
|
+
"by-load": "by load",
|
24
|
+
"by-memory": "by memory",
|
25
|
+
"by-usage": "by usage",
|
26
|
+
"by-size": "by size"
|
11
27
|
}
|
@@ -7,5 +7,21 @@
|
|
7
7
|
"title.pools": "Пулы",
|
8
8
|
"title.metrics": "Метрики",
|
9
9
|
|
10
|
-
"top-groups.empty-data": "Нет групп"
|
10
|
+
"top-groups.empty-data": "Нет групп",
|
11
|
+
|
12
|
+
"top": "Топ",
|
13
|
+
|
14
|
+
"nodes": "узлов",
|
15
|
+
"shards": "шардов",
|
16
|
+
"groups": "групп",
|
17
|
+
"queries": "запросов",
|
18
|
+
"tables": "таблиц",
|
19
|
+
|
20
|
+
"by-pools-usage": "по использованию пулов",
|
21
|
+
"by-cpu-time": "по времени cpu",
|
22
|
+
"by-cpu-usage": "по использованию cpu",
|
23
|
+
"by-load": "по нагрузке",
|
24
|
+
"by-memory": "по памяти",
|
25
|
+
"by-usage": "по потреблению",
|
26
|
+
"by-size": "по размеру"
|
11
27
|
}
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import {useMemo} from 'react';
|
1
2
|
import cn from 'bem-cn-lite';
|
2
3
|
|
3
4
|
import DataTable, {Column} from '@gravity-ui/react-data-table';
|
@@ -28,6 +29,16 @@ interface SchemaViewerProps {
|
|
28
29
|
}
|
29
30
|
|
30
31
|
export const SchemaViewer = ({keyColumnIds = [], columns = [], type}: SchemaViewerProps) => {
|
32
|
+
// Keys should be displayd by their order in keyColumnIds (Primary Key)
|
33
|
+
const keyColumnsOrderValues = useMemo(() => {
|
34
|
+
return keyColumnIds.reduce<Record<number, number>>((result, keyColumnId, index) => {
|
35
|
+
// Put columns with negative values, so they will be the first with ascending sort
|
36
|
+
// Minus keyColumnIds.length for the first key, -1 for the last
|
37
|
+
result[keyColumnId] = index - keyColumnIds.length;
|
38
|
+
return result;
|
39
|
+
}, {});
|
40
|
+
}, [keyColumnIds]);
|
41
|
+
|
31
42
|
let dataTableColumns: Column<TColumnDescription>[] = [
|
32
43
|
{
|
33
44
|
name: SchemaViewerColumns.id,
|
@@ -36,8 +47,11 @@ export const SchemaViewer = ({keyColumnIds = [], columns = [], type}: SchemaView
|
|
36
47
|
{
|
37
48
|
name: SchemaViewerColumns.key,
|
38
49
|
width: 40,
|
50
|
+
// Table should start with key columns on sort click
|
51
|
+
defaultOrder: DataTable.ASCENDING,
|
39
52
|
sortAccessor: (row) => {
|
40
|
-
|
53
|
+
// Values in keyColumnsOrderValues are always negative, so it will be 1 for not key columns
|
54
|
+
return (row.Id && keyColumnsOrderValues[row.Id]) || 1;
|
41
55
|
},
|
42
56
|
render: ({row}) => {
|
43
57
|
return row.Id && keyColumnIds.includes(row.Id) ? (
|
@@ -58,6 +72,8 @@ export const SchemaViewer = ({keyColumnIds = [], columns = [], type}: SchemaView
|
|
58
72
|
{
|
59
73
|
name: SchemaViewerColumns.notNull,
|
60
74
|
width: 100,
|
75
|
+
// Table should start with notNull columns on sort click
|
76
|
+
defaultOrder: DataTable.DESCENDING,
|
61
77
|
render: ({row}) => {
|
62
78
|
if (row.NotNull) {
|
63
79
|
return '\u2713';
|
@@ -75,22 +91,14 @@ export const SchemaViewer = ({keyColumnIds = [], columns = [], type}: SchemaView
|
|
75
91
|
);
|
76
92
|
}
|
77
93
|
|
78
|
-
// Display key columns first
|
79
|
-
const tableData = columns.sort((column) => {
|
80
|
-
if (column.Id && keyColumnIds.includes(column.Id)) {
|
81
|
-
return 1;
|
82
|
-
}
|
83
|
-
return -1;
|
84
|
-
});
|
85
|
-
|
86
94
|
return (
|
87
95
|
<div className={b()}>
|
88
96
|
<DataTable
|
89
97
|
theme="yandex-cloud"
|
90
|
-
data={
|
98
|
+
data={columns}
|
91
99
|
columns={dataTableColumns}
|
92
100
|
settings={DEFAULT_TABLE_SETTINGS}
|
93
|
-
initialSortOrder={{columnId: SchemaViewerColumns.key, order: DataTable.
|
101
|
+
initialSortOrder={{columnId: SchemaViewerColumns.key, order: DataTable.ASCENDING}}
|
94
102
|
/>
|
95
103
|
</div>
|
96
104
|
);
|
@@ -0,0 +1,121 @@
|
|
1
|
+
import {parseGroupsStatsQueryResponse} from '../utils';
|
2
|
+
|
3
|
+
describe('parseGroupsStatsQueryResponse', () => {
|
4
|
+
const columns = [
|
5
|
+
{
|
6
|
+
name: 'PDiskFilter',
|
7
|
+
type: 'Utf8?',
|
8
|
+
},
|
9
|
+
{
|
10
|
+
name: 'ErasureSpecies',
|
11
|
+
type: 'Utf8?',
|
12
|
+
},
|
13
|
+
{
|
14
|
+
name: 'CurrentAvailableSize',
|
15
|
+
type: 'Uint64?',
|
16
|
+
},
|
17
|
+
{
|
18
|
+
name: 'CurrentAllocatedSize',
|
19
|
+
type: 'Uint64?',
|
20
|
+
},
|
21
|
+
{
|
22
|
+
name: 'CurrentGroupsCreated',
|
23
|
+
type: 'Uint32?',
|
24
|
+
},
|
25
|
+
{
|
26
|
+
name: 'AvailableGroupsToCreate',
|
27
|
+
type: 'Uint32?',
|
28
|
+
},
|
29
|
+
];
|
30
|
+
|
31
|
+
// 2 disk types and 2 erasure types
|
32
|
+
const dataSet1 = {
|
33
|
+
columns,
|
34
|
+
result: [
|
35
|
+
['Type:SSD', 'block-4-2', '1000', '2000', 100, 50],
|
36
|
+
['Type:ROT', 'block-4-2', '2000', '1000', 50, 0],
|
37
|
+
['Type:ROT', 'mirror-3of4', '1000', '0', 15, 0],
|
38
|
+
['Type:SSD', 'mirror-3of4', '1000', '0', 5, 50],
|
39
|
+
['Type:ROT', 'mirror-3-dc', null, null, null, 0],
|
40
|
+
['Type:SSD', 'mirror-3-dc', null, null, null, 0],
|
41
|
+
],
|
42
|
+
};
|
43
|
+
|
44
|
+
// 2 disk types and 1 erasure types, but with additional disks params
|
45
|
+
const dataSet2 = {
|
46
|
+
columns,
|
47
|
+
result: [
|
48
|
+
['Type:ROT,SharedWithOs:0,ReadCentric:0,Kind:0', 'mirror-3-dc', '1000', '500', 16, 16],
|
49
|
+
['Type:ROT,SharedWithOs:1,ReadCentric:0,Kind:0', 'mirror-3-dc', '2000', '1000', 8, 24],
|
50
|
+
['Type:SSD', 'mirror-3-dc', '3000', '400', 2, 10],
|
51
|
+
['Type:ROT', 'mirror-3-dc', null, null, null, 32],
|
52
|
+
['Type:ROT', 'block-4-2', null, null, null, 20],
|
53
|
+
['Type:SSD', 'block-4-2', null, null, null, 0],
|
54
|
+
],
|
55
|
+
};
|
56
|
+
const parsedDataSet1 = {
|
57
|
+
SSD: {
|
58
|
+
'block-4-2': {
|
59
|
+
diskType: 'SSD',
|
60
|
+
erasure: 'block-4-2',
|
61
|
+
createdGroups: 100,
|
62
|
+
totalGroups: 150,
|
63
|
+
allocatedSize: 2000,
|
64
|
+
availableSize: 1000,
|
65
|
+
},
|
66
|
+
'mirror-3of4': {
|
67
|
+
diskType: 'SSD',
|
68
|
+
erasure: 'mirror-3of4',
|
69
|
+
createdGroups: 5,
|
70
|
+
totalGroups: 55,
|
71
|
+
allocatedSize: 0,
|
72
|
+
availableSize: 1000,
|
73
|
+
},
|
74
|
+
},
|
75
|
+
HDD: {
|
76
|
+
'block-4-2': {
|
77
|
+
diskType: 'HDD',
|
78
|
+
erasure: 'block-4-2',
|
79
|
+
createdGroups: 50,
|
80
|
+
totalGroups: 50,
|
81
|
+
allocatedSize: 1000,
|
82
|
+
availableSize: 2000,
|
83
|
+
},
|
84
|
+
'mirror-3of4': {
|
85
|
+
diskType: 'HDD',
|
86
|
+
erasure: 'mirror-3of4',
|
87
|
+
createdGroups: 15,
|
88
|
+
totalGroups: 15,
|
89
|
+
allocatedSize: 0,
|
90
|
+
availableSize: 1000,
|
91
|
+
},
|
92
|
+
},
|
93
|
+
};
|
94
|
+
|
95
|
+
const parsedDataSet2 = {
|
96
|
+
HDD: {
|
97
|
+
'mirror-3-dc': {
|
98
|
+
diskType: 'HDD',
|
99
|
+
erasure: 'mirror-3-dc',
|
100
|
+
createdGroups: 24,
|
101
|
+
totalGroups: 64,
|
102
|
+
allocatedSize: 1500,
|
103
|
+
availableSize: 3000,
|
104
|
+
},
|
105
|
+
},
|
106
|
+
SSD: {
|
107
|
+
'mirror-3-dc': {
|
108
|
+
diskType: 'SSD',
|
109
|
+
erasure: 'mirror-3-dc',
|
110
|
+
createdGroups: 2,
|
111
|
+
totalGroups: 12,
|
112
|
+
allocatedSize: 400,
|
113
|
+
availableSize: 3000,
|
114
|
+
},
|
115
|
+
},
|
116
|
+
};
|
117
|
+
it('should correctly parse data', () => {
|
118
|
+
expect(parseGroupsStatsQueryResponse(dataSet1)).toEqual(parsedDataSet1);
|
119
|
+
expect(parseGroupsStatsQueryResponse(dataSet2)).toEqual(parsedDataSet2);
|
120
|
+
});
|
121
|
+
});
|
@@ -3,6 +3,7 @@ import type {Reducer} from 'redux';
|
|
3
3
|
import '../../../services/api';
|
4
4
|
import {createRequestActionTypes, createApiRequest} from '../../utils';
|
5
5
|
import type {ClusterAction, ClusterState} from './types';
|
6
|
+
import {createSelectClusterGroupsQuery, parseGroupsStatsQueryResponse} from './utils';
|
6
7
|
|
7
8
|
export const FETCH_CLUSTER = createRequestActionTypes('cluster', 'FETCH_CLUSTER');
|
8
9
|
|
@@ -17,9 +18,12 @@ const cluster: Reducer<ClusterState, ClusterAction> = (state = initialState, act
|
|
17
18
|
};
|
18
19
|
}
|
19
20
|
case FETCH_CLUSTER.SUCCESS: {
|
21
|
+
const {clusterData, groupsStats} = action.data;
|
22
|
+
|
20
23
|
return {
|
21
24
|
...state,
|
22
|
-
data:
|
25
|
+
data: clusterData,
|
26
|
+
groupsStats,
|
23
27
|
loading: false,
|
24
28
|
wasLoaded: true,
|
25
29
|
error: undefined,
|
@@ -42,8 +46,48 @@ const cluster: Reducer<ClusterState, ClusterAction> = (state = initialState, act
|
|
42
46
|
};
|
43
47
|
|
44
48
|
export function getClusterInfo(clusterName?: string) {
|
49
|
+
async function requestClusterData() {
|
50
|
+
// Error here is handled by createApiRequest
|
51
|
+
const clusterData = await window.api.getClusterInfo(clusterName);
|
52
|
+
|
53
|
+
try {
|
54
|
+
const clusterRoot = clusterData.Domain;
|
55
|
+
|
56
|
+
// Without domain we cannot get stats from system tables
|
57
|
+
if (!clusterRoot) {
|
58
|
+
return {
|
59
|
+
clusterData,
|
60
|
+
};
|
61
|
+
}
|
62
|
+
|
63
|
+
const query = createSelectClusterGroupsQuery(clusterRoot);
|
64
|
+
|
65
|
+
// Normally query request should be fulfilled within 300-400ms even on very big clusters
|
66
|
+
// Table with stats is supposed to be very small (less than 10 rows)
|
67
|
+
// So we batch this request with cluster request to prevent possible layout shifts, if data is missing
|
68
|
+
const groupsStatsResponse = await window.api.sendQuery({
|
69
|
+
schema: 'modern',
|
70
|
+
query: query,
|
71
|
+
database: clusterRoot,
|
72
|
+
action: 'execute-scan',
|
73
|
+
});
|
74
|
+
|
75
|
+
return {
|
76
|
+
clusterData,
|
77
|
+
groupsStats: parseGroupsStatsQueryResponse(groupsStatsResponse),
|
78
|
+
};
|
79
|
+
} catch {
|
80
|
+
// Doesn't return groups stats on error
|
81
|
+
// It could happen if user doesn't have access rights
|
82
|
+
// Or there are no system tables in cluster root
|
83
|
+
return {
|
84
|
+
clusterData,
|
85
|
+
};
|
86
|
+
}
|
87
|
+
}
|
88
|
+
|
45
89
|
return createApiRequest({
|
46
|
-
request:
|
90
|
+
request: requestClusterData(),
|
47
91
|
actions: FETCH_CLUSTER,
|
48
92
|
});
|
49
93
|
}
|
@@ -1,14 +1,39 @@
|
|
1
|
-
import {FETCH_CLUSTER} from './cluster';
|
2
|
-
|
3
1
|
import type {TClusterInfo} from '../../../types/api/cluster';
|
4
|
-
import type {ApiRequestAction} from '../../utils';
|
5
2
|
import type {IResponseError} from '../../../types/api/error';
|
3
|
+
import type {ApiRequestAction} from '../../utils';
|
4
|
+
|
5
|
+
import {FETCH_CLUSTER} from './cluster';
|
6
|
+
|
7
|
+
export interface DiskErasureGroupsStats {
|
8
|
+
diskType: string;
|
9
|
+
erasure: string;
|
10
|
+
createdGroups: number;
|
11
|
+
totalGroups: number;
|
12
|
+
allocatedSize: number;
|
13
|
+
availableSize: number;
|
14
|
+
}
|
15
|
+
|
16
|
+
/** Keys - erasure types */
|
17
|
+
export type DiskGroupsStats = Record<string, DiskErasureGroupsStats>;
|
18
|
+
|
19
|
+
/** Keys - PDisks types */
|
20
|
+
export type ClusterGroupsStats = Record<string, DiskGroupsStats>;
|
6
21
|
|
7
22
|
export interface ClusterState {
|
8
23
|
loading: boolean;
|
9
24
|
wasLoaded: boolean;
|
10
25
|
data?: TClusterInfo;
|
11
26
|
error?: IResponseError;
|
27
|
+
groupsStats?: ClusterGroupsStats;
|
28
|
+
}
|
29
|
+
|
30
|
+
export interface HandledClusterResponse {
|
31
|
+
clusterData: TClusterInfo;
|
32
|
+
groupsStats: ClusterGroupsStats;
|
12
33
|
}
|
13
34
|
|
14
|
-
export type ClusterAction = ApiRequestAction<
|
35
|
+
export type ClusterAction = ApiRequestAction<
|
36
|
+
typeof FETCH_CLUSTER,
|
37
|
+
HandledClusterResponse,
|
38
|
+
IResponseError
|
39
|
+
>;
|
@@ -0,0 +1,88 @@
|
|
1
|
+
import type {ExecuteQueryResponse} from '../../../types/api/query';
|
2
|
+
import {parseQueryAPIExecuteResponse} from '../../../utils/query';
|
3
|
+
|
4
|
+
import type {ClusterGroupsStats} from './types';
|
5
|
+
|
6
|
+
export const createSelectClusterGroupsQuery = (clusterRoot: string) => {
|
7
|
+
return `
|
8
|
+
SELECT
|
9
|
+
PDiskFilter,
|
10
|
+
ErasureSpecies,
|
11
|
+
CurrentAvailableSize,
|
12
|
+
CurrentAllocatedSize,
|
13
|
+
CurrentGroupsCreated,
|
14
|
+
AvailableGroupsToCreate
|
15
|
+
FROM \`${clusterRoot}/.sys/ds_storage_stats\`
|
16
|
+
ORDER BY CurrentGroupsCreated DESC;
|
17
|
+
`;
|
18
|
+
};
|
19
|
+
|
20
|
+
const getDiskType = (rawTypeString: string) => {
|
21
|
+
// Check if value math regexp and put disk type in type group
|
22
|
+
const diskTypeRe = /^Type:(?<type>[A-Za-z]+)/;
|
23
|
+
|
24
|
+
const diskType = rawTypeString.match(diskTypeRe)?.groups?.['type'];
|
25
|
+
|
26
|
+
if (diskType === 'ROT') {
|
27
|
+
// Display ROT as HDD
|
28
|
+
return 'HDD';
|
29
|
+
}
|
30
|
+
|
31
|
+
return diskType;
|
32
|
+
};
|
33
|
+
|
34
|
+
export const parseGroupsStatsQueryResponse = (
|
35
|
+
data: ExecuteQueryResponse<'modern'>,
|
36
|
+
): ClusterGroupsStats => {
|
37
|
+
const parsedData = parseQueryAPIExecuteResponse(data).result;
|
38
|
+
const result: ClusterGroupsStats = {};
|
39
|
+
|
40
|
+
parsedData?.forEach((stats) => {
|
41
|
+
const {
|
42
|
+
PDiskFilter,
|
43
|
+
ErasureSpecies: erasure,
|
44
|
+
CurrentAvailableSize,
|
45
|
+
CurrentAllocatedSize,
|
46
|
+
CurrentGroupsCreated,
|
47
|
+
AvailableGroupsToCreate,
|
48
|
+
} = stats;
|
49
|
+
|
50
|
+
const createdGroups = Number(CurrentGroupsCreated) || 0;
|
51
|
+
const availableGroupsToCreate = Number(AvailableGroupsToCreate) || 0;
|
52
|
+
const totalGroups = createdGroups + availableGroupsToCreate;
|
53
|
+
const allocatedSize = Number(CurrentAllocatedSize) || 0;
|
54
|
+
const availableSize = Number(CurrentAvailableSize) || 0;
|
55
|
+
const diskType = PDiskFilter && typeof PDiskFilter === 'string' && getDiskType(PDiskFilter);
|
56
|
+
|
57
|
+
if (diskType && erasure && typeof erasure === 'string' && createdGroups) {
|
58
|
+
const preparedStats = {
|
59
|
+
diskType,
|
60
|
+
erasure,
|
61
|
+
createdGroups,
|
62
|
+
totalGroups,
|
63
|
+
allocatedSize,
|
64
|
+
availableSize,
|
65
|
+
};
|
66
|
+
|
67
|
+
if (result[diskType]) {
|
68
|
+
if (result[diskType][erasure]) {
|
69
|
+
const currentValue = {...result[diskType][erasure]};
|
70
|
+
result[diskType][erasure] = {
|
71
|
+
diskType,
|
72
|
+
erasure,
|
73
|
+
createdGroups: currentValue.createdGroups + createdGroups,
|
74
|
+
totalGroups: currentValue.totalGroups + totalGroups,
|
75
|
+
allocatedSize: currentValue.allocatedSize + allocatedSize,
|
76
|
+
availableSize: currentValue.availableSize + availableSize,
|
77
|
+
};
|
78
|
+
} else {
|
79
|
+
result[diskType][erasure] = preparedStats;
|
80
|
+
}
|
81
|
+
} else {
|
82
|
+
result[diskType] = {[erasure]: preparedStats};
|
83
|
+
}
|
84
|
+
}
|
85
|
+
});
|
86
|
+
|
87
|
+
return result;
|
88
|
+
};
|