ydb-embedded-ui 4.22.0 → 4.24.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. package/CHANGELOG.md +20 -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/services/api.ts +1 -2
  23. package/dist/store/reducers/cluster/__test__/parseGroupsStatsQueryResponse.test.ts +121 -0
  24. package/dist/store/reducers/cluster/cluster.ts +46 -2
  25. package/dist/store/reducers/cluster/types.ts +29 -4
  26. package/dist/store/reducers/cluster/utils.ts +88 -0
  27. package/dist/store/reducers/settings/settings.ts +14 -6
  28. package/dist/types/api/cluster.ts +3 -0
  29. package/dist/utils/hooks/index.ts +1 -0
  30. package/dist/utils/hooks/useSearchQuery.ts +9 -0
  31. package/dist/utils/settings.ts +4 -1
  32. package/package.json +1 -1
@@ -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
  );
@@ -34,11 +34,10 @@ import type {StorageApiRequestParams} from '../store/reducers/storage/types';
34
34
 
35
35
  import {backend as BACKEND} from '../store';
36
36
  import {prepareSortValue} from '../utils/filters';
37
+ import {settingsApi} from '../utils/settings';
37
38
 
38
39
  const config = {withCredentials: !window.custom_backend};
39
40
 
40
- const settingsApi = window.web_version ? window.systemSettings?.settingsApi : undefined;
41
-
42
41
  type AxiosOptions = {
43
42
  concurrentId?: string;
44
43
  };
@@ -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
+ };
@@ -21,7 +21,12 @@ import {
21
21
  import '../../../services/api';
22
22
  import {parseJson} from '../../../utils/utils';
23
23
  import {QUERY_ACTIONS, QUERY_MODES} from '../../../utils/query';
24
- import {readSavedSettingsValue, systemSettings, userSettings} from '../../../utils/settings';
24
+ import {
25
+ readSavedSettingsValue,
26
+ settingsApi,
27
+ systemSettings,
28
+ userSettings,
29
+ } from '../../../utils/settings';
25
30
 
26
31
  import {TENANT_PAGES_IDS} from '../tenant/constants';
27
32
 
@@ -115,13 +120,16 @@ export const setSettingValue = (
115
120
  name: string,
116
121
  value: string,
117
122
  ): ThunkAction<void, RootState, unknown, SetSettingValueAction> => {
118
- return (dispatch, getState) => {
123
+ return (dispatch) => {
119
124
  dispatch({type: SET_SETTING_VALUE, data: {name, value}});
120
- const {singleClusterMode} = getState();
121
- if (singleClusterMode) {
122
- localStorage.setItem(name, value);
123
- } else {
125
+
126
+ // If there is no settingsApi, use localStorage
127
+ if (settingsApi) {
124
128
  window.api.postSetting(name, value);
129
+ } else {
130
+ try {
131
+ localStorage.setItem(name, value);
132
+ } catch {}
125
133
  }
126
134
  };
127
135
  };
@@ -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
+ };
@@ -3,8 +3,11 @@ import {getValueFromLS} from './utils';
3
3
  export const userSettings = window.userSettings || {};
4
4
  export const systemSettings = window.systemSettings || {};
5
5
 
6
+ export const settingsApi = window.web_version ? systemSettings.settingsApi : undefined;
7
+
6
8
  export function readSavedSettingsValue(key: string, defaultValue?: string) {
7
- const savedValue = window.web_version ? userSettings[key] : getValueFromLS(key);
9
+ // If there is no settingsApi, use localStorage
10
+ const savedValue = settingsApi ? userSettings[key] : getValueFromLS(key);
8
11
 
9
12
  return savedValue ?? defaultValue;
10
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-embedded-ui",
3
- "version": "4.22.0",
3
+ "version": "4.24.0",
4
4
  "files": [
5
5
  "dist"
6
6
  ],