ydb-embedded-ui 4.22.0 → 4.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/components/ProgressViewer/ProgressViewer.tsx +1 -1
  3. package/dist/containers/Cluster/Cluster.tsx +2 -0
  4. package/dist/containers/Cluster/ClusterInfo/ClusterInfo.scss +14 -5
  5. package/dist/containers/Cluster/ClusterInfo/ClusterInfo.tsx +104 -24
  6. package/dist/containers/Cluster/ClusterInfoSkeleton/ClusterInfoSkeleton.tsx +1 -1
  7. package/dist/containers/Cluster/i18n/en.json +16 -0
  8. package/dist/containers/Cluster/i18n/index.ts +11 -0
  9. package/dist/containers/Cluster/i18n/ru.json +16 -0
  10. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByCpu.tsx +18 -3
  11. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopNodesByLoad.tsx +18 -3
  12. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx +17 -1
  13. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx +20 -1
  14. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TopNodesByMemory.tsx +18 -3
  15. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantOverviewTableLayout.tsx +2 -1
  16. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopGroups.tsx +19 -2
  17. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx +8 -1
  18. package/dist/containers/Tenant/Diagnostics/TenantOverview/getSectionTitle.tsx +28 -0
  19. package/dist/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json +17 -1
  20. package/dist/containers/Tenant/Diagnostics/TenantOverview/i18n/ru.json +17 -1
  21. package/dist/containers/Tenant/Schema/SchemaViewer/SchemaViewer.tsx +19 -11
  22. package/dist/store/reducers/cluster/__test__/parseGroupsStatsQueryResponse.test.ts +121 -0
  23. package/dist/store/reducers/cluster/cluster.ts +46 -2
  24. package/dist/store/reducers/cluster/types.ts +29 -4
  25. package/dist/store/reducers/cluster/utils.ts +88 -0
  26. package/dist/types/api/cluster.ts +3 -0
  27. package/dist/utils/hooks/index.ts +1 -0
  28. package/dist/utils/hooks/useSearchQuery.ts +9 -0
  29. 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: 'DC',
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: 'Tablets',
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: 'Databases',
150
+ label: i18n('databases'),
67
151
  value: cluster.Tenants,
68
152
  });
69
153
  }
70
154
 
71
155
  info.push(
72
156
  {
73
- label: 'Nodes',
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: 'Load',
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: 'Storage',
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: 'Links',
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: 'Versions',
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 = 7, className}: ClusterInfoSkeletonProps) => (
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="Top nodes by pools usage"
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="Top nodes by load"
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="Top queries by cpu time"
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="Top shards by cpu usage"
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="Top nodes by memory"
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: string;
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="Top groups by usage"
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="Top tables by size"
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
- return row.Id && keyColumnIds.includes(row.Id) ? 1 : 0;
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={tableData}
98
+ data={columns}
91
99
  columns={dataTableColumns}
92
100
  settings={DEFAULT_TABLE_SETTINGS}
93
- initialSortOrder={{columnId: SchemaViewerColumns.key, order: DataTable.DESCENDING}}
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: action.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: window.api.getClusterInfo(clusterName),
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<typeof FETCH_CLUSTER, TClusterInfo, IResponseError>;
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
+ };
@@ -32,6 +32,9 @@ export interface TClusterInfo {
32
32
  /** uint64 */
33
33
  Tablets?: string;
34
34
 
35
+ /** Cluster root database */
36
+ Domain?: string;
37
+
35
38
  Balancer?: string; // additional
36
39
  Solomon?: string; // additional
37
40
  }
@@ -3,6 +3,7 @@ export * from './useTypedSelector';
3
3
  export * from './useSetting';
4
4
  export * from './useQueryModes';
5
5
  export * from './useTableSort';
6
+ export * from './useSearchQuery';
6
7
 
7
8
  export * from './useNodesRequestParams';
8
9
  export * from './useStorageRequestParams';
@@ -0,0 +1,9 @@
1
+ import {useLocation} from 'react-router';
2
+
3
+ import {parseQuery} from '../../routes';
4
+
5
+ export const useSearchQuery = () => {
6
+ const location = useLocation();
7
+
8
+ return parseQuery(location);
9
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-embedded-ui",
3
- "version": "4.22.0",
3
+ "version": "4.23.0",
4
4
  "files": [
5
5
  "dist"
6
6
  ],