ydb-embedded-ui 1.9.0 → 1.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/components/IndexInfoViewer/IndexInfoViewer.tsx +10 -7
  3. package/dist/components/InfoViewer/InfoViewer.scss +1 -2
  4. package/dist/components/InfoViewer/utils.ts +18 -10
  5. package/dist/containers/Storage/Pdisk/Pdisk.tsx +25 -33
  6. package/dist/containers/Storage/Vdisk/Vdisk.js +2 -0
  7. package/dist/containers/Tablet/Tablet.js +2 -2
  8. package/dist/containers/Tenant/Diagnostics/DetailedOverview/DetailedOverview.tsx +15 -14
  9. package/dist/containers/Tenant/Diagnostics/DiagnosticsPages.ts +24 -14
  10. package/dist/containers/Tenant/Diagnostics/HotKeys/HotKeys.js +3 -3
  11. package/dist/containers/Tenant/Diagnostics/Overview/Overview.tsx +20 -13
  12. package/dist/containers/Tenant/Diagnostics/TopShards/TopShards.js +80 -10
  13. package/dist/containers/Tenant/QueryEditor/QueryEditor.js +12 -2
  14. package/dist/containers/Tenant/Schema/SchemaInfoViewer/SchemaInfoViewer.js +164 -42
  15. package/dist/containers/Tenant/Schema/SchemaInfoViewer/SchemaInfoViewer.scss +18 -0
  16. package/dist/containers/Tenant/utils/schema.ts +73 -28
  17. package/dist/containers/Tenant/utils/schemaActions.ts +45 -32
  18. package/dist/services/api.js +13 -9
  19. package/dist/store/reducers/executeQuery.js +4 -3
  20. package/dist/store/reducers/executeTopQueries.js +1 -1
  21. package/dist/store/reducers/olapStats.js +5 -1
  22. package/dist/store/reducers/preview.js +1 -1
  23. package/dist/store/reducers/settings.js +20 -13
  24. package/dist/store/reducers/shardsWorkload.js +32 -4
  25. package/dist/types/api/schema.ts +123 -4
  26. package/dist/types/api/storage.ts +1 -1
  27. package/dist/utils/constants.js +4 -0
  28. package/dist/utils/index.js +7 -3
  29. package/dist/utils/pdisk.ts +2 -2
  30. package/package.json +2 -2
@@ -3,63 +3,185 @@ import PropTypes from 'prop-types';
3
3
  import cn from 'bem-cn-lite';
4
4
  import './SchemaInfoViewer.scss';
5
5
 
6
- import {formatCPU, formatBytes} from '../../../../utils';
6
+ import {formatCPU, formatBytes, formatNumber, formatBps} from '../../../../utils';
7
7
 
8
- import InfoViewer from '../../../../components/InfoViewer/InfoViewer';
8
+ import {InfoViewer, createInfoFormatter} from '../../../../components/InfoViewer';
9
9
 
10
10
  const b = cn('schema-info-viewer');
11
11
 
12
+ const formatTabletMetricsItem = createInfoFormatter({
13
+ values: {
14
+ CPU: formatCPU,
15
+ Memory: formatBytes,
16
+ Storage: formatBytes,
17
+ Network: formatBps,
18
+ ReadThroughput: formatBps,
19
+ WriteThroughput: formatBps,
20
+ },
21
+ defaultValueFormatter: formatNumber,
22
+ });
23
+
24
+ const formatFollowerGroupItem = createInfoFormatter({
25
+ values: {
26
+ FollowerCount: formatNumber,
27
+ },
28
+ });
29
+
30
+ const formatPartitionConfigItem = createInfoFormatter({
31
+ values: {
32
+ FollowerCount: formatNumber,
33
+ CrossDataCenterFollowerCount: formatNumber,
34
+ },
35
+ });
36
+
37
+ const formatTableStatsItem = createInfoFormatter({
38
+ values: {
39
+ DataSize: formatBytes,
40
+ IndexSize: formatBytes,
41
+ LastAccessTime: (value) => value > 0 ? new Date(Number(value)).toUTCString() : 'N/A',
42
+ LastUpdateTime: (value) => value > 0 ? new Date(Number(value)).toUTCString() : 'N/A',
43
+ },
44
+ defaultValueFormatter: formatNumber,
45
+ });
46
+
47
+ const formatTableStats = (fields) => Object.entries(fields)
48
+ .map(([label, value]) => formatTableStatsItem(label, value))
49
+ .filter(({value}) => Boolean(value));
50
+
12
51
  class SchemaInfoViewer extends React.Component {
13
52
  static propTypes = {
14
53
  data: PropTypes.object.isRequired,
15
54
  };
16
- formatTabletMetricsValue = (key, value) => {
17
- if (key === 'CPU') {
18
- return formatCPU(value);
19
- } else if (key === 'Memory' || key === 'Storage') {
20
- return formatBytes(value);
21
- } else {
22
- return value;
55
+
56
+ renderItem(itemData, title) {
57
+ if (!Array.isArray(itemData) || !itemData.length) {
58
+ return null;
23
59
  }
24
- };
60
+
61
+ return (
62
+ <div className={b('item')}>
63
+ <InfoViewer
64
+ title={title}
65
+ info={itemData}
66
+ />
67
+ </div>
68
+ );
69
+ }
70
+
71
+ renderContent(data) {
72
+ const {PathDescription = {}} = data;
73
+ const {TableStats = {}, TabletMetrics = {}, Table: {PartitionConfig = {}} = {}} = PathDescription;
74
+ const {
75
+ PartCount,
76
+ RowCount,
77
+ DataSize,
78
+ IndexSize,
79
+
80
+ LastAccessTime,
81
+ LastUpdateTime,
82
+
83
+ ImmediateTxCompleted,
84
+ PlannedTxCompleted,
85
+ TxRejectedByOverload,
86
+ TxRejectedBySpace,
87
+ TxCompleteLagMsec,
88
+ InFlightTxCount,
89
+
90
+ RowUpdates,
91
+ RowDeletes,
92
+ RowReads,
93
+ RangeReads,
94
+ RangeReadRows,
95
+
96
+ ...restTableStats
97
+ } = TableStats;
98
+ const {FollowerGroups, FollowerCount, CrossDataCenterFollowerCount} = PartitionConfig;
99
+
100
+ const tableStatsInfo = [
101
+ formatTableStats({
102
+ PartCount,
103
+ RowCount,
104
+ DataSize,
105
+ IndexSize,
106
+ }),
107
+ formatTableStats({
108
+ LastAccessTime,
109
+ LastUpdateTime,
110
+ }),
111
+ formatTableStats({
112
+ ImmediateTxCompleted,
113
+ PlannedTxCompleted,
114
+ TxRejectedByOverload,
115
+ TxRejectedBySpace,
116
+ TxCompleteLagMsec,
117
+ InFlightTxCount,
118
+ }),
119
+ formatTableStats({
120
+ RowUpdates,
121
+ RowDeletes,
122
+ RowReads,
123
+ RangeReads,
124
+ RangeReadRows,
125
+ }),
126
+ formatTableStats(restTableStats),
127
+ ];
128
+
129
+ const tabletMetricsInfo = Object.keys(TabletMetrics).map((key) =>
130
+ formatTabletMetricsItem(key, TabletMetrics[key])
131
+ );
132
+
133
+ const partitionConfigInfo = [];
134
+
135
+ if (Array.isArray(FollowerGroups) && FollowerGroups.length > 0) {
136
+ partitionConfigInfo.push(...Object.keys(FollowerGroups[0]).map((key) =>
137
+ formatFollowerGroupItem(key, FollowerGroups[0][key])
138
+ ));
139
+ } else if (FollowerCount !== undefined) {
140
+ partitionConfigInfo.push(
141
+ formatPartitionConfigItem('FollowerCount', FollowerCount)
142
+ );
143
+ } else if (CrossDataCenterFollowerCount !== undefined) {
144
+ partitionConfigInfo.push(
145
+ formatPartitionConfigItem('CrossDataCenterFollowerCount', CrossDataCenterFollowerCount)
146
+ );
147
+ }
148
+
149
+ if ([
150
+ tabletMetricsInfo,
151
+ partitionConfigInfo,
152
+ tableStatsInfo.flat(),
153
+ ].flat().length === 0) {
154
+ return (
155
+ <div className={b('item')}>Empty</div>
156
+ );
157
+ }
158
+
159
+ return (
160
+ <div className={b('row')}>
161
+ {tabletMetricsInfo.length > 0 || partitionConfigInfo.length > 0 ? (
162
+ <div className={b('col')}>
163
+ {this.renderItem(tabletMetricsInfo, 'Tablet Metrics')}
164
+ {this.renderItem(partitionConfigInfo, 'Partition Config')}
165
+ </div>
166
+ ) : null}
167
+ <div className={b('col')}>
168
+ {tableStatsInfo.map((info, index) => (
169
+ <React.Fragment key={index}>
170
+ {this.renderItem(info, index === 0 ? 'Table Stats' : undefined)}
171
+ </React.Fragment>
172
+ ))}
173
+ </div>
174
+ </div>
175
+ );
176
+ }
177
+
25
178
  render() {
26
179
  const {data} = this.props;
27
180
 
28
181
  if (data) {
29
- const {PathDescription = {}} = data;
30
- const {TableStats = {}, TabletMetrics = {}} = PathDescription;
31
- const {PartCount, ...restTableStats} = TableStats;
32
-
33
- const priorityInfo = [{
34
- label: 'PartCount',
35
- value: PartCount,
36
- }].filter(({value}) => value !== undefined);
37
-
38
- const tableStatsInfo = Object.keys(restTableStats).map((key) => ({
39
- label: key,
40
- value: TableStats[key].toString(),
41
- }));
42
-
43
- const tabletMetricsInfo = Object.keys(TabletMetrics).map((key) => ({
44
- label: key,
45
- value: this.formatTabletMetricsValue(key, TabletMetrics[key].toString()),
46
- }));
47
-
48
- const generalInfo = [
49
- ...priorityInfo,
50
- ...tabletMetricsInfo,
51
- ...tableStatsInfo,
52
- ];
53
-
54
182
  return (
55
183
  <div className={b()}>
56
- <div className={b('item')}>
57
- {generalInfo.length ? (
58
- <InfoViewer info={generalInfo}></InfoViewer>
59
- ) : (
60
- <div>Empty</div>
61
- )}
62
- </div>
184
+ {this.renderContent(data)}
63
185
  </div>
64
186
  );
65
187
  } else {
@@ -1,6 +1,24 @@
1
1
  .schema-info-viewer {
2
2
  overflow: auto;
3
3
 
4
+ &__row {
5
+ display: flex;
6
+ flex-wrap: wrap;
7
+ justify-content: flex-start;
8
+ align-items: flex-start;
9
+ }
10
+
11
+ &__col {
12
+ display: flex;
13
+ flex-direction: column;
14
+ justify-content: flex-start;
15
+ align-items: flex-start;
16
+
17
+ & + & {
18
+ margin-left: 50px;
19
+ }
20
+ }
21
+
4
22
  &__item {
5
23
  margin-bottom: 20px;
6
24
 
@@ -1,43 +1,88 @@
1
1
  import type {NavigationTreeNodeType} from 'ydb-ui-components';
2
2
  import {EPathSubType, EPathType} from '../../../types/api/schema';
3
3
 
4
- const mapTablePathSubTypeToNavigationTreeType = (subType?: EPathSubType) => {
5
- switch (subType) {
6
- case EPathSubType.EPathSubTypeSyncIndexImplTable:
7
- case EPathSubType.EPathSubTypeAsyncIndexImplTable:
8
- return 'index_table';
9
- default:
10
- return 'table';
11
- }
4
+ // this file contains verbose mappings that are typed in a way that ensures
5
+ // correctness when a new node type or a new path type is added
6
+ // TS will error if a new entity is added but not mapped here
7
+
8
+ const pathSubTypeToNodeType: Record<EPathSubType, NavigationTreeNodeType | undefined> = {
9
+ [EPathSubType.EPathSubTypeSyncIndexImplTable]: 'index_table',
10
+ [EPathSubType.EPathSubTypeAsyncIndexImplTable]: 'index_table',
11
+
12
+ [EPathSubType.EPathSubTypeStreamImpl]: undefined,
13
+ [EPathSubType.EPathSubTypeEmpty]: undefined,
14
+ };
15
+
16
+ const pathTypeToNodeType: Record<EPathType, NavigationTreeNodeType | undefined> = {
17
+ [EPathType.EPathTypeInvalid]: undefined,
18
+
19
+ [EPathType.EPathTypeSubDomain]: 'database',
20
+ [EPathType.EPathTypeExtSubDomain]: 'database',
21
+
22
+ [EPathType.EPathTypeDir]: 'directory',
23
+ [EPathType.EPathTypeColumnStore]: 'directory',
24
+
25
+ [EPathType.EPathTypeTable]: 'table',
26
+
27
+ [EPathType.EPathTypeTableIndex]: 'index',
28
+
29
+ [EPathType.EPathTypeColumnTable]: 'column_table',
30
+
31
+ [EPathType.EPathTypeCdcStream]: 'topic',
12
32
  };
13
33
 
14
34
  export const mapPathTypeToNavigationTreeType = (
15
35
  type: EPathType = EPathType.EPathTypeDir,
16
36
  subType?: EPathSubType,
17
37
  defaultType: NavigationTreeNodeType = 'directory'
18
- ): NavigationTreeNodeType => {
19
- switch (type) {
20
- case EPathType.EPathTypeSubDomain:
21
- return 'database';
22
- case EPathType.EPathTypeTable:
23
- case EPathType.EPathTypeColumnTable:
24
- return mapTablePathSubTypeToNavigationTreeType(subType);
25
- case EPathType.EPathTypeDir:
26
- case EPathType.EPathTypeColumnStore:
27
- return 'directory';
28
- case EPathType.EPathTypeTableIndex:
29
- return 'index';
30
- default:
31
- return defaultType;
32
- }
38
+ ): NavigationTreeNodeType =>
39
+ (subType && pathSubTypeToNodeType[subType]) || pathTypeToNodeType[type] || defaultType;
40
+
41
+ // ====================
42
+
43
+ const pathTypeToIsTable: Record<EPathType, boolean> = {
44
+ [EPathType.EPathTypeTable]: true,
45
+ [EPathType.EPathTypeColumnTable]: true,
46
+
47
+ [EPathType.EPathTypeInvalid]: false,
48
+ [EPathType.EPathTypeDir]: false,
49
+ [EPathType.EPathTypeSubDomain]: false,
50
+ [EPathType.EPathTypeTableIndex]: false,
51
+ [EPathType.EPathTypeExtSubDomain]: false,
52
+ [EPathType.EPathTypeColumnStore]: false,
53
+ [EPathType.EPathTypeCdcStream]: false,
33
54
  };
34
55
 
35
- export const isTableType = (type?: EPathType) =>
36
- mapPathTypeToNavigationTreeType(type) === 'table';
56
+ export const isTableType = (pathType?: EPathType) =>
57
+ (pathType && pathTypeToIsTable[pathType]) ?? false;
58
+
59
+ // ====================
60
+
61
+ const pathSubTypeToIsIndexImpl: Record<EPathSubType, boolean> = {
62
+ [EPathSubType.EPathSubTypeSyncIndexImplTable]: true,
63
+ [EPathSubType.EPathSubTypeAsyncIndexImplTable]: true,
64
+
65
+ [EPathSubType.EPathSubTypeStreamImpl]: false,
66
+ [EPathSubType.EPathSubTypeEmpty]: false,
67
+ };
37
68
 
38
69
  export const isIndexTable = (subType?: EPathSubType) =>
39
- mapTablePathSubTypeToNavigationTreeType(subType) === 'index_table';
70
+ (subType && pathSubTypeToIsIndexImpl[subType]) ?? false;
71
+
72
+ // ====================
73
+
74
+ const pathTypeToIsColumn: Record<EPathType, boolean> = {
75
+ [EPathType.EPathTypeColumnStore]: true,
76
+ [EPathType.EPathTypeColumnTable]: true,
77
+
78
+ [EPathType.EPathTypeInvalid]: false,
79
+ [EPathType.EPathTypeDir]: false,
80
+ [EPathType.EPathTypeTable]: false,
81
+ [EPathType.EPathTypeSubDomain]: false,
82
+ [EPathType.EPathTypeTableIndex]: false,
83
+ [EPathType.EPathTypeExtSubDomain]: false,
84
+ [EPathType.EPathTypeCdcStream]: false,
85
+ };
40
86
 
41
87
  export const isColumnEntityType = (type?: EPathType) =>
42
- type === EPathType.EPathTypeColumnStore ||
43
- type === EPathType.EPathTypeColumnTable;
88
+ (type && pathTypeToIsColumn[type]) ?? false;
@@ -1,5 +1,5 @@
1
1
  import {Dispatch} from 'react';
2
- import type {NavigationTreeNodeType} from 'ydb-ui-components';
2
+ import type {NavigationTreeNodeType, NavigationTreeProps} from 'ydb-ui-components';
3
3
 
4
4
  import {changeUserInput} from '../../../store/reducers/executeQuery';
5
5
  import {setShowPreview} from '../../../store/reducers/schema';
@@ -73,6 +73,8 @@ const bindActions = (
73
73
  };
74
74
  };
75
75
 
76
+ type ActionsSet = ReturnType<Required<NavigationTreeProps>['getActions']>;
77
+
76
78
  export const getActions = (
77
79
  dispatch: Dispatch<any>,
78
80
  setActivePath: (path: string) => void,
@@ -81,35 +83,46 @@ export const getActions = (
81
83
  const actions = bindActions(path, dispatch, setActivePath);
82
84
  const copyItem = {text: 'Copy path', action: actions.copyPath};
83
85
 
84
- switch (type) {
85
- case 'database':
86
- case 'directory':
87
- return [
88
- [
89
- copyItem,
90
- ],
91
- [
92
- {text: 'Create table...', action: actions.createTable},
93
- ],
94
- ];
95
- case 'table':
96
- return [
97
- [
98
- {text: 'Open preview', action: actions.openPreview},
99
- copyItem,
100
- ],
101
- [
102
- {text: 'Alter table...', action: actions.alterTable},
103
- {text: 'Select query...', action: actions.selectQuery},
104
- {text: 'Upsert query...', action: actions.upsertQuery},
105
- ],
106
- ];
107
- case 'index_table':
108
- return [
109
- copyItem,
110
- ];
111
- case 'index':
112
- default:
113
- return [];
114
- }
86
+ const DIR_SET: ActionsSet = [
87
+ [
88
+ copyItem,
89
+ ],
90
+ [
91
+ {text: 'Create table...', action: actions.createTable},
92
+ ],
93
+ ];
94
+ const TABLE_SET: ActionsSet = [
95
+ [
96
+ {text: 'Open preview', action: actions.openPreview},
97
+ copyItem,
98
+ ],
99
+ [
100
+ {text: 'Alter table...', action: actions.alterTable},
101
+ {text: 'Select query...', action: actions.selectQuery},
102
+ {text: 'Upsert query...', action: actions.upsertQuery},
103
+ ],
104
+ ];
105
+
106
+ const JUST_COPY: ActionsSet = [
107
+ copyItem,
108
+ ];
109
+
110
+ const EMPTY_SET: ActionsSet = [];
111
+
112
+ // verbose mapping to guarantee a correct actions set for new node types
113
+ // TS will error when a new type is added in the lib but is not mapped here
114
+ const nodeTypeToActions: Record<NavigationTreeNodeType, ActionsSet> = {
115
+ database: DIR_SET,
116
+ directory: DIR_SET,
117
+
118
+ table: TABLE_SET,
119
+ column_table: TABLE_SET,
120
+
121
+ index_table: JUST_COPY,
122
+ topic: JUST_COPY,
123
+
124
+ index: EMPTY_SET,
125
+ };
126
+
127
+ return nodeTypeToActions[type];
115
128
  };
@@ -83,7 +83,6 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
83
83
  path,
84
84
  enums: true,
85
85
  backup: false,
86
- partition_config: false,
87
86
  partition_stats: false,
88
87
  partitioning_info: false,
89
88
  },
@@ -147,14 +146,19 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
147
146
  state: 0,
148
147
  });
149
148
  }
150
- sendQuery(query, database, action, stats) {
151
- return this.post(this.getPath('/viewer/json/query'), {
152
- query,
153
- database,
154
- action,
155
- stats,
156
- timeout: 600000,
157
- });
149
+ sendQuery({query, database, action, stats}, {concurrentId} = {}) {
150
+ return this.post(
151
+ this.getPath('/viewer/json/query'),
152
+ {
153
+ query,
154
+ database,
155
+ action,
156
+ stats,
157
+ timeout: 600000,
158
+ },
159
+ null,
160
+ {concurrentId},
161
+ );
158
162
  }
159
163
  getExplainQuery(query, database) {
160
164
  return this.post(this.getPath('/viewer/json/query'), {
@@ -1,7 +1,8 @@
1
1
  import {createRequestActionTypes, createApiRequest} from '../utils';
2
2
  import '../../services/api';
3
3
  import {getValueFromLS, parseJson} from '../../utils/utils';
4
- import {QUERIES_HISTORY_KEY} from '../../utils/constants';
4
+ import {QUERIES_HISTORY_KEY, QUERY_INITIAL_RUN_ACTION_KEY} from '../../utils/constants';
5
+ import {readSavedSettingsValue} from './settings';
5
6
 
6
7
  const MAXIMUM_QUERIES_IN_HISTORY = 20;
7
8
 
@@ -39,7 +40,7 @@ const initialState = {
39
40
  ? MAXIMUM_QUERIES_IN_HISTORY - 1
40
41
  : queriesHistoryInitial.length - 1,
41
42
  },
42
- runAction: RUN_ACTIONS_VALUES.script,
43
+ runAction: readSavedSettingsValue(QUERY_INITIAL_RUN_ACTION_KEY, RUN_ACTIONS_VALUES.script),
43
44
  monacoHotKey: null,
44
45
  };
45
46
 
@@ -141,7 +142,7 @@ const executeQuery = (state = initialState, action) => {
141
142
 
142
143
  export const sendQuery = ({query, database, action}) => {
143
144
  return createApiRequest({
144
- request: window.api.sendQuery(query, database, action, 'profile'),
145
+ request: window.api.sendQuery({query, database, action, stats: 'profile'}),
145
146
  actions: SEND_QUERY,
146
147
  dataHandler: (result) => {
147
148
  const resultData = result.result ?? result;
@@ -47,7 +47,7 @@ const executeTopQueries = (state = initialState, action) => {
47
47
 
48
48
  export const sendQuery = ({query, database, action}) => {
49
49
  return createApiRequest({
50
- request: window.api.sendQuery(query, database, action),
50
+ request: window.api.sendQuery({query, database, action}),
51
51
  actions: SEND_QUERY,
52
52
  dataHandler: (result) => {
53
53
  if (result && typeof result === 'string') {
@@ -52,7 +52,11 @@ const olapStats = (state = initialState, action) => {
52
52
 
53
53
  export const getOlapStats = ({path = ''}) => {
54
54
  return createApiRequest({
55
- request: window.api.sendQuery(createOlatStatsQuery(path), path, queryAction),
55
+ request: window.api.sendQuery({
56
+ query: createOlatStatsQuery(path),
57
+ database: path,
58
+ action: queryAction,
59
+ }),
56
60
  actions: FETCH_OLAP_STATS,
57
61
  dataHandler: (result) => {
58
62
  if (result && typeof result === 'string') {
@@ -47,7 +47,7 @@ const preview = (state = initialState, action) => {
47
47
 
48
48
  export const sendQuery = ({query, database, action}) => {
49
49
  return createApiRequest({
50
- request: window.api.sendQuery(query, database, action),
50
+ request: window.api.sendQuery({query, database, action}),
51
51
  actions: SEND_QUERY,
52
52
  dataHandler: (data) => {
53
53
  if (!Array.isArray(data)) {
@@ -1,4 +1,11 @@
1
- import {ALL, defaultUserSettings, SAVED_QUERIES_KEY, THEME_KEY, TENANT_INITIAL_TAB_KEY} from '../../utils/constants';
1
+ import {
2
+ defaultUserSettings,
3
+ ALL,
4
+ SAVED_QUERIES_KEY,
5
+ THEME_KEY,
6
+ TENANT_INITIAL_TAB_KEY,
7
+ QUERY_INITIAL_RUN_ACTION_KEY,
8
+ } from '../../utils/constants';
2
9
  import '../../services/api';
3
10
  import {getValueFromLS} from '../../utils/utils';
4
11
 
@@ -7,24 +14,24 @@ const SET_SETTING_VALUE = 'settings/SET_VALUE';
7
14
 
8
15
  const userSettings = window.userSettings || {};
9
16
  const systemSettings = window.systemSettings || {};
10
- const theme = window.web_version
11
- ? userSettings.theme || 'light'
12
- : getValueFromLS(THEME_KEY, 'light');
13
- const savedQueries = window.web_version
14
- ? userSettings[SAVED_QUERIES_KEY]
15
- : getValueFromLS(SAVED_QUERIES_KEY, '[]');
16
- const savedTenantGeneralTab = window.web_version
17
- ? userSettings[TENANT_INITIAL_TAB_KEY]
18
- : getValueFromLS(TENANT_INITIAL_TAB_KEY);
17
+
18
+ export function readSavedSettingsValue(key, defaultValue) {
19
+ const savedValue = window.web_version
20
+ ? userSettings[key]
21
+ : getValueFromLS(key);
22
+
23
+ return savedValue ?? defaultValue;
24
+ }
19
25
 
20
26
  export const initialState = {
21
27
  problemFilter: ALL,
22
28
  userSettings: {
23
29
  ...defaultUserSettings,
24
30
  ...userSettings,
25
- theme,
26
- [SAVED_QUERIES_KEY]: savedQueries,
27
- [TENANT_INITIAL_TAB_KEY]: savedTenantGeneralTab,
31
+ theme: readSavedSettingsValue(THEME_KEY, 'light'),
32
+ [SAVED_QUERIES_KEY]: readSavedSettingsValue(SAVED_QUERIES_KEY, '[]'),
33
+ [TENANT_INITIAL_TAB_KEY]: readSavedSettingsValue(TENANT_INITIAL_TAB_KEY),
34
+ [QUERY_INITIAL_RUN_ACTION_KEY]: readSavedSettingsValue(QUERY_INITIAL_RUN_ACTION_KEY),
28
35
  },
29
36
  systemSettings,
30
37
  };
@@ -9,8 +9,30 @@ const initialState = {
9
9
  wasLoaded: false,
10
10
  };
11
11
 
12
- function createShardQuery(path) {
13
- return `SELECT Path, TabletId, CPUCores FROM \`.sys/partition_stats\` WHERE Path='${path}' OR Path LIKE '${path}/%' ORDER BY CPUCores DESC LIMIT 20`;
12
+ function formatSortOrder({columnId, order}) {
13
+ return `${columnId} ${order}`;
14
+ }
15
+
16
+ function createShardQuery(path, sortOrder, tenantName) {
17
+ const orderBy = Array.isArray(sortOrder) ?
18
+ `ORDER BY ${sortOrder.map(formatSortOrder).join(', ')}` :
19
+ '';
20
+
21
+ const pathSelect = tenantName ?
22
+ `CAST(SUBSTRING(CAST(Path AS String), ${tenantName.length}) AS Utf8) AS Path` :
23
+ 'Path';
24
+
25
+ return `SELECT
26
+ ${pathSelect},
27
+ TabletId,
28
+ CPUCores,
29
+ DataSize
30
+ FROM \`.sys/partition_stats\`
31
+ WHERE
32
+ Path='${path}'
33
+ OR Path LIKE '${path}/%'
34
+ ${orderBy}
35
+ LIMIT 20`;
14
36
  }
15
37
 
16
38
  const queryAction = 'execute-scan';
@@ -51,9 +73,15 @@ const shardsWorkload = (state = initialState, action) => {
51
73
  }
52
74
  };
53
75
 
54
- export const sendShardQuery = ({database, path = ''}) => {
76
+ export const sendShardQuery = ({database, path = '', sortOrder}) => {
55
77
  return createApiRequest({
56
- request: window.api.sendQuery(createShardQuery(path), database, queryAction),
78
+ request: window.api.sendQuery({
79
+ query: createShardQuery(path, sortOrder, database),
80
+ database,
81
+ action: queryAction,
82
+ }, {
83
+ concurrentId: 'topShards',
84
+ }),
57
85
  actions: SEND_SHARD_QUERY,
58
86
  dataHandler: (result) => {
59
87
  if (result && typeof result === 'string') {