ydb-embedded-ui 3.3.3 → 3.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (22) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/containers/Storage/StorageGroups/StorageGroups.tsx +8 -0
  3. package/dist/containers/Tenant/Diagnostics/Diagnostics.tsx +3 -3
  4. package/dist/containers/Tenant/Diagnostics/DiagnosticsPages.ts +7 -7
  5. package/dist/containers/Tenant/Diagnostics/TopShards/Filters/Filters.scss +8 -0
  6. package/dist/containers/Tenant/Diagnostics/TopShards/Filters/Filters.tsx +56 -0
  7. package/dist/containers/Tenant/Diagnostics/TopShards/Filters/index.ts +1 -0
  8. package/dist/containers/Tenant/Diagnostics/{OverloadedShards/OverloadedShards.scss → TopShards/TopShards.scss} +2 -10
  9. package/dist/containers/Tenant/Diagnostics/{OverloadedShards/OverloadedShards.tsx → TopShards/TopShards.tsx} +61 -31
  10. package/dist/containers/Tenant/Diagnostics/TopShards/i18n/en.json +6 -0
  11. package/dist/containers/Tenant/Diagnostics/{OverloadedShards → TopShards}/i18n/index.ts +1 -1
  12. package/dist/containers/Tenant/Diagnostics/TopShards/i18n/ru.json +6 -0
  13. package/dist/containers/Tenant/Diagnostics/TopShards/index.ts +1 -0
  14. package/dist/store/reducers/authentication.js +0 -15
  15. package/dist/store/reducers/shardsWorkload.ts +28 -2
  16. package/dist/store/state-url-mapping.js +3 -0
  17. package/dist/types/store/shardsWorkload.ts +6 -0
  18. package/dist/utils/typecheckers.ts +5 -0
  19. package/package.json +1 -1
  20. package/dist/containers/Tenant/Diagnostics/OverloadedShards/i18n/en.json +0 -4
  21. package/dist/containers/Tenant/Diagnostics/OverloadedShards/i18n/ru.json +0 -4
  22. package/dist/containers/Tenant/Diagnostics/OverloadedShards/index.ts +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.3.4](https://github.com/ydb-platform/ydb-embedded-ui/compare/v3.3.3...v3.3.4) (2023-02-16)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **OverloadedShards:** rename to top shards ([ffa4f27](https://github.com/ydb-platform/ydb-embedded-ui/commit/ffa4f27f2cf0a5e12b2800c81bf61b1d3c25912c))
9
+ * **StorageGroups:** display Erasure ([4a7ebc0](https://github.com/ydb-platform/ydb-embedded-ui/commit/4a7ebc08b87fe75af83df70a38ebd486d64d6d4e))
10
+ * **TopShards:** switch between history and immediate data ([eeb9bb0](https://github.com/ydb-platform/ydb-embedded-ui/commit/eeb9bb0911b9e889b633558c9d3c13f986f72bfe))
11
+
3
12
  ## [3.3.3](https://github.com/ydb-platform/ydb-embedded-ui/compare/v3.3.2...v3.3.3) (2023-02-08)
4
13
 
5
14
 
@@ -28,6 +28,7 @@ import './StorageGroups.scss';
28
28
  enum TableColumnsIds {
29
29
  PoolName = 'PoolName',
30
30
  Type = 'Type',
31
+ ErasureSpecies = 'ErasureSpecies',
31
32
  GroupID = 'GroupID',
32
33
  Used = 'Used',
33
34
  Limit = 'Limit',
@@ -53,6 +54,7 @@ interface StorageGroupsProps {
53
54
  const tableColumnsNames: Record<TableColumnsIdsValues, string> = {
54
55
  PoolName: 'Pool Name',
55
56
  Type: 'Type',
57
+ ErasureSpecies: 'Erasure',
56
58
  GroupID: 'Group ID',
57
59
  Used: 'Used',
58
60
  Limit: 'Limit',
@@ -146,6 +148,12 @@ function StorageGroups({
146
148
  </>
147
149
  ),
148
150
  },
151
+ {
152
+ name: TableColumnsIds.ErasureSpecies,
153
+ header: tableColumnsNames[TableColumnsIds.ErasureSpecies],
154
+ render: ({row}) => (row.ErasureSpecies ? row.ErasureSpecies : '-'),
155
+ align: DataTable.LEFT,
156
+ },
149
157
  {
150
158
  name: TableColumnsIds.Missing,
151
159
  header: tableColumnsNames[TableColumnsIds.Missing],
@@ -12,7 +12,7 @@ import {Loader} from '../../../components/Loader';
12
12
  import {TopQueries} from './TopQueries';
13
13
  //@ts-ignore
14
14
  import DetailedOverview from './DetailedOverview/DetailedOverview';
15
- import {OverloadedShards} from './OverloadedShards';
15
+ import {TopShards} from './TopShards';
16
16
  //@ts-ignore
17
17
  import Storage from '../../Storage/Storage';
18
18
  //@ts-ignore
@@ -124,8 +124,8 @@ function Diagnostics(props: DiagnosticsProps) {
124
124
  />
125
125
  );
126
126
  }
127
- case GeneralPagesIds.overloadedShards: {
128
- return <OverloadedShards tenantPath={tenantNameString} type={type} />;
127
+ case GeneralPagesIds.topShards: {
128
+ return <TopShards tenantPath={tenantNameString} type={type} />;
129
129
  }
130
130
  case GeneralPagesIds.nodes: {
131
131
  return (
@@ -3,7 +3,7 @@ import {EPathType} from '../../../types/api/schema';
3
3
  export enum GeneralPagesIds {
4
4
  'overview' = 'Overview',
5
5
  'topQueries' = 'topQueries',
6
- 'overloadedShards' = 'overloadedShards',
6
+ 'topShards' = 'topShards',
7
7
  'nodes' = 'Nodes',
8
8
  'tablets' = 'Tablets',
9
9
  'storage' = 'Storage',
@@ -29,9 +29,9 @@ const topQueries = {
29
29
  title: 'Top queries',
30
30
  };
31
31
 
32
- const overloadedShards = {
33
- id: GeneralPagesIds.overloadedShards,
34
- title: 'Overloaded shards',
32
+ const topShards = {
33
+ id: GeneralPagesIds.topShards,
34
+ title: 'Top shards',
35
35
  };
36
36
 
37
37
  const nodes = {
@@ -75,7 +75,7 @@ const consumers = {
75
75
  export const DATABASE_PAGES = [
76
76
  overview,
77
77
  topQueries,
78
- overloadedShards,
78
+ topShards,
79
79
  nodes,
80
80
  tablets,
81
81
  storage,
@@ -83,9 +83,9 @@ export const DATABASE_PAGES = [
83
83
  describe,
84
84
  ];
85
85
 
86
- export const TABLE_PAGES = [overview, overloadedShards, graph, tablets, hotKeys, describe];
86
+ export const TABLE_PAGES = [overview, topShards, graph, tablets, hotKeys, describe];
87
87
 
88
- export const DIR_PAGES = [overview, overloadedShards, describe];
88
+ export const DIR_PAGES = [overview, topShards, describe];
89
89
 
90
90
  export const CDC_STREAM_PAGES = [overview, consumers, describe];
91
91
  export const TOPIC_PAGES = [overview, consumers, describe];
@@ -0,0 +1,8 @@
1
+ .top-shards {
2
+ &__filters {
3
+ display: flex;
4
+ flex-wrap: wrap;
5
+ align-items: baseline;
6
+ gap: 16px;
7
+ }
8
+ }
@@ -0,0 +1,56 @@
1
+ import {RadioButton} from '@gravity-ui/uikit';
2
+
3
+ import {DateRange, DateRangeValues} from '../../../../../components/DateRange';
4
+
5
+ import {
6
+ EShardsWorkloadMode,
7
+ IShardsWorkloadFilters,
8
+ } from '../../../../../types/store/shardsWorkload';
9
+
10
+ import {isEnumMember} from '../../../../../utils/typecheckers';
11
+
12
+ import i18n from '../i18n';
13
+ import {b} from '../TopShards';
14
+
15
+ import './Filters.scss';
16
+
17
+ interface FiltersProps {
18
+ value: IShardsWorkloadFilters;
19
+ onChange: (value: Partial<IShardsWorkloadFilters>) => void;
20
+ className?: string;
21
+ }
22
+
23
+ export const Filters = ({value, onChange, className}: FiltersProps) => {
24
+ const handleModeChange = (mode: string) => {
25
+ if (!isEnumMember(EShardsWorkloadMode, mode)) {
26
+ const values = Object.values(EShardsWorkloadMode).join(', ');
27
+ throw new Error(`Unexpected TopShards mode "${mode}". Should be one of: ${values}`);
28
+ }
29
+
30
+ onChange({mode});
31
+ };
32
+
33
+ const handleDateRangeChange = (dateRange: DateRangeValues) => {
34
+ onChange({
35
+ mode: EShardsWorkloadMode.History,
36
+ ...dateRange,
37
+ });
38
+ };
39
+
40
+ const from = value.mode === EShardsWorkloadMode.Immediate ? undefined : value.from;
41
+ const to = value.mode === EShardsWorkloadMode.Immediate ? undefined : value.to;
42
+
43
+ return (
44
+ <div className={b('filters', className)}>
45
+ <RadioButton value={value.mode} onUpdate={handleModeChange}>
46
+ <RadioButton.Option value={EShardsWorkloadMode.Immediate}>
47
+ {i18n('filters.mode.immediate')}
48
+ </RadioButton.Option>
49
+ <RadioButton.Option value={EShardsWorkloadMode.History}>
50
+ {i18n('filters.mode.history')}
51
+ </RadioButton.Option>
52
+ </RadioButton>
53
+ <DateRange from={from} to={to} onChange={handleDateRangeChange} />
54
+ </div>
55
+ );
56
+ };
@@ -0,0 +1 @@
1
+ export * from './Filters';
@@ -1,6 +1,7 @@
1
- .overloaded-shards {
1
+ .top-shards {
2
2
  display: flex;
3
3
  flex-direction: column;
4
+ gap: 10px;
4
5
 
5
6
  height: 100%;
6
7
 
@@ -11,15 +12,6 @@
11
12
  justify-content: center;
12
13
  }
13
14
 
14
- &__controls {
15
- display: flex;
16
- flex-wrap: wrap;
17
- align-items: baseline;
18
- gap: 16px;
19
-
20
- margin-bottom: 10px;
21
- }
22
-
23
15
  &__table {
24
16
  overflow: auto;
25
17
  flex-grow: 1;
@@ -5,7 +5,6 @@ import cn from 'bem-cn-lite';
5
5
  import DataTable, {Column, Settings, SortOrder} from '@gravity-ui/react-data-table';
6
6
  import {Loader} from '@gravity-ui/uikit';
7
7
 
8
- import {DateRange, DateRangeValues} from '../../../../components/DateRange';
9
8
  import {InternalLink} from '../../../../components/InternalLink';
10
9
 
11
10
  import HistoryContext from '../../../../contexts/HistoryContext';
@@ -18,7 +17,7 @@ import {
18
17
  setShardsQueryFilters,
19
18
  } from '../../../../store/reducers/shardsWorkload';
20
19
  import {setCurrentSchemaPath, getSchema} from '../../../../store/reducers/schema';
21
- import type {IShardsWorkloadFilters} from '../../../../types/store/shardsWorkload';
20
+ import {EShardsWorkloadMode, IShardsWorkloadFilters} from '../../../../types/store/shardsWorkload';
22
21
 
23
22
  import type {EPathType} from '../../../../types/api/schema';
24
23
 
@@ -31,10 +30,12 @@ import {getDefaultNodePath} from '../../../Node/NodePages';
31
30
 
32
31
  import {isColumnEntityType} from '../../utils/schema';
33
32
 
33
+ import {Filters} from './Filters';
34
+
34
35
  import i18n from './i18n';
35
- import './OverloadedShards.scss';
36
+ import './TopShards.scss';
36
37
 
37
- const b = cn('overloaded-shards');
38
+ export const b = cn('top-shards');
38
39
  const bLink = cn('yc-link');
39
40
 
40
41
  const TABLE_SETTINGS: Settings = {
@@ -83,12 +84,18 @@ function dataTableToStringSortOrder(value: SortOrder | SortOrder[] = []) {
83
84
  return sortOrders.map(({columnId}) => columnId).join(',');
84
85
  }
85
86
 
86
- interface OverloadedShardsProps {
87
+ function fillDateRangeFor(value: IShardsWorkloadFilters) {
88
+ value.to = Date.now();
89
+ value.from = value.to - HOUR_IN_SECONDS * 1000;
90
+ return value;
91
+ }
92
+
93
+ interface TopShardsProps {
87
94
  tenantPath: string;
88
95
  type?: EPathType;
89
96
  }
90
97
 
91
- export const OverloadedShards = ({tenantPath, type}: OverloadedShardsProps) => {
98
+ export const TopShards = ({tenantPath, type}: TopShardsProps) => {
92
99
  const dispatch = useDispatch();
93
100
 
94
101
  const {autorefresh, currentSchemaPath} = useTypedSelector((state) => state.schema);
@@ -101,17 +108,20 @@ export const OverloadedShards = ({tenantPath, type}: OverloadedShardsProps) => {
101
108
  wasLoaded,
102
109
  } = useTypedSelector((state) => state.shardsWorkload);
103
110
 
104
- // default date range should be the last hour, but shouldn't propagate into URL until user interacts with the control
111
+ // default filters shouldn't propagate into URL until user interacts with the control
105
112
  // redux initial value can't be used, as it synchronizes with URL
106
113
  const [filters, setFilters] = useState<IShardsWorkloadFilters>(() => {
107
- if (!storeFilters?.from && !storeFilters?.to) {
108
- return {
109
- from: Date.now() - HOUR_IN_SECONDS * 1000,
110
- to: Date.now(),
111
- };
114
+ const defaultValue = {...storeFilters};
115
+
116
+ if (!defaultValue.mode) {
117
+ defaultValue.mode = EShardsWorkloadMode.Immediate;
118
+ }
119
+
120
+ if (!defaultValue.from && !defaultValue.to) {
121
+ fillDateRangeFor(defaultValue);
112
122
  }
113
123
 
114
- return storeFilters;
124
+ return defaultValue;
115
125
  });
116
126
 
117
127
  const [sortOrder, setSortOrder] = useState(tableColumnsNames.CPUCores);
@@ -144,18 +154,34 @@ export const OverloadedShards = ({tenantPath, type}: OverloadedShardsProps) => {
144
154
  const history = useContext(HistoryContext);
145
155
 
146
156
  const onSort = (newSortOrder?: SortOrder | SortOrder[]) => {
147
- // omit information about sort order to disable ASC order, only DESC makes sense for overloaded shards
157
+ // omit information about sort order to disable ASC order, only DESC makes sense for top shards
148
158
  // use a string (and not the DataTable default format) to prevent reference change,
149
159
  // which would cause an excess state change, to avoid repeating requests
150
160
  setSortOrder(dataTableToStringSortOrder(newSortOrder));
151
161
  };
152
162
 
153
- const handleDateRangeChange = (value: DateRangeValues) => {
163
+ const handleFiltersChange = (value: Partial<IShardsWorkloadFilters>) => {
164
+ const newStateValue = {...value};
165
+ const isDateRangePristine =
166
+ !storeFilters.from && !storeFilters.to && !value.from && !value.to;
167
+
168
+ if (isDateRangePristine) {
169
+ switch (value.mode) {
170
+ case EShardsWorkloadMode.Immediate:
171
+ newStateValue.from = newStateValue.to = undefined;
172
+ break;
173
+ case EShardsWorkloadMode.History:
174
+ // should default to the current datetime every time history mode activates
175
+ fillDateRangeFor(newStateValue);
176
+ break;
177
+ }
178
+ }
179
+
154
180
  dispatch(setShardsQueryFilters(value));
155
- setFilters(value);
181
+ setFilters((state) => ({...state, ...newStateValue}));
156
182
  };
157
183
 
158
- const tableColumns: Column<any>[] = useMemo(() => {
184
+ const tableColumns = useMemo(() => {
159
185
  const onSchemaClick = (schemaPath: string) => {
160
186
  return () => {
161
187
  dispatch(setCurrentSchemaPath(schemaPath));
@@ -164,7 +190,7 @@ export const OverloadedShards = ({tenantPath, type}: OverloadedShardsProps) => {
164
190
  };
165
191
  };
166
192
 
167
- return [
193
+ const columns: Column<any>[] = [
168
194
  {
169
195
  name: tableColumnsNames.Path,
170
196
  render: ({value: relativeNodePath}) => {
@@ -217,23 +243,29 @@ export const OverloadedShards = ({tenantPath, type}: OverloadedShardsProps) => {
217
243
  align: DataTable.RIGHT,
218
244
  sortable: false,
219
245
  },
220
- {
221
- name: tableColumnsNames.PeakTime,
222
- render: ({value}) => formatDateTime(new Date(value as string).valueOf()),
223
- sortable: false,
224
- },
225
246
  {
226
247
  name: tableColumnsNames.InFlightTxCount,
227
248
  render: ({value}) => formatNumber(value as number),
228
249
  align: DataTable.RIGHT,
229
250
  sortable: false,
230
251
  },
231
- {
252
+ ];
253
+
254
+ if (filters.mode === EShardsWorkloadMode.History) {
255
+ // after NodeId
256
+ columns.splice(5, 0, {
257
+ name: tableColumnsNames.PeakTime,
258
+ render: ({value}) => formatDateTime(new Date(value as string).valueOf()),
259
+ sortable: false,
260
+ });
261
+ columns.push({
232
262
  name: tableColumnsNames.IntervalEnd,
233
263
  render: ({value}) => formatDateTime(new Date(value as string).getTime()),
234
- }
235
- ];
236
- }, [dispatch, history, tenantPath]);
264
+ });
265
+ }
266
+
267
+ return columns;
268
+ }, [dispatch, filters.mode, history, tenantPath]);
237
269
 
238
270
  const renderLoader = () => {
239
271
  return (
@@ -272,10 +304,8 @@ export const OverloadedShards = ({tenantPath, type}: OverloadedShardsProps) => {
272
304
 
273
305
  return (
274
306
  <div className={b()}>
275
- <div className={b('controls')}>
276
- {i18n('description')}
277
- <DateRange from={filters.from} to={filters.to} onChange={handleDateRangeChange} />
278
- </div>
307
+ <Filters value={filters} onChange={handleFiltersChange} />
308
+ {filters.mode === EShardsWorkloadMode.History && <div>{i18n('description')}</div>}
279
309
  {renderContent()}
280
310
  </div>
281
311
  );
@@ -0,0 +1,6 @@
1
+ {
2
+ "no-data": "No data",
3
+ "filters.mode.immediate": "Immediate",
4
+ "filters.mode.history": "Historical",
5
+ "description": "Historical data only tracks shards with CPU load over 70%"
6
+ }
@@ -3,7 +3,7 @@ import {i18n, Lang} from '../../../../../utils/i18n';
3
3
  import en from './en.json';
4
4
  import ru from './ru.json';
5
5
 
6
- const COMPONENT = 'ydb-diagnostics-overloaded-shards';
6
+ const COMPONENT = 'ydb-diagnostics-top-shards';
7
7
 
8
8
  i18n.registerKeyset(Lang.En, COMPONENT, en);
9
9
  i18n.registerKeyset(Lang.Ru, COMPONENT, ru);
@@ -0,0 +1,6 @@
1
+ {
2
+ "no-data": "Нет данных",
3
+ "filters.mode.immediate": "Мгновенные",
4
+ "filters.mode.history": "Исторические",
5
+ "description": "Исторические данные хранятся только о шардах с загрузкой CPU выше 70%"
6
+ }
@@ -0,0 +1 @@
1
+ export * from './TopShards';
@@ -34,21 +34,6 @@ const authentication = function (state = initialState, action) {
34
34
  }
35
35
  };
36
36
 
37
- export const setIsNotAuthenticated = () => {
38
- return (dispatch) => {
39
- dispatch({
40
- type: SET_UNAUTHENTICATED.SUCCESS,
41
- });
42
- };
43
- };
44
- export const setIsAuthenticated = () => {
45
- return (dispatch) => {
46
- dispatch({
47
- type: SET_AUTHENTICATED.SUCCESS,
48
- });
49
- };
50
- };
51
-
52
37
  export const authenticate = (user, password) => {
53
38
  return createApiRequest({
54
39
  request: window.api.authenticate(user, password),
@@ -6,6 +6,7 @@ import type {
6
6
  IShardsWorkloadFilters,
7
7
  IShardsWorkloadState,
8
8
  } from '../../types/store/shardsWorkload';
9
+ import {EShardsWorkloadMode} from '../../types/store/shardsWorkload';
9
10
 
10
11
  import {parseQueryAPIExecuteResponse} from '../../utils/query';
11
12
 
@@ -51,7 +52,7 @@ function getFiltersConditions(filters?: IShardsWorkloadFilters) {
51
52
  return conditions.join(' AND ');
52
53
  }
53
54
 
54
- function createShardQuery(
55
+ function createShardQueryHistorical(
55
56
  path: string,
56
57
  filters?: IShardsWorkloadFilters,
57
58
  sortOrder?: SortOrder[],
@@ -85,6 +86,28 @@ ${orderBy}
85
86
  LIMIT 20`;
86
87
  }
87
88
 
89
+ function createShardQueryImmediate(path: string, sortOrder?: SortOrder[], tenantName?: string) {
90
+ const pathSelect = tenantName
91
+ ? `CAST(SUBSTRING(CAST(Path AS String), ${tenantName.length}) AS Utf8) AS Path`
92
+ : 'Path';
93
+
94
+ const orderBy = sortOrder ? `ORDER BY ${sortOrder.map(formatSortOrder).join(', ')}` : '';
95
+
96
+ return `SELECT
97
+ ${pathSelect},
98
+ TabletId,
99
+ CPUCores,
100
+ DataSize,
101
+ NodeId,
102
+ InFlightTxCount
103
+ FROM \`.sys/partition_stats\`
104
+ WHERE
105
+ Path='${path}'
106
+ OR Path LIKE '${path}/%'
107
+ ${orderBy}
108
+ LIMIT 20`;
109
+ }
110
+
88
111
  const queryAction = 'execute-scan';
89
112
 
90
113
  const shardsWorkload: Reducer<IShardsWorkloadState, IShardsWorkloadAction> = (
@@ -147,7 +170,10 @@ export const sendShardQuery = ({database, path = '', sortOrder, filters}: SendSh
147
170
  request: window.api.sendQuery(
148
171
  {
149
172
  schema: 'modern',
150
- query: createShardQuery(path, filters, sortOrder, database),
173
+ query:
174
+ filters?.mode === EShardsWorkloadMode.Immediate
175
+ ? createShardQueryImmediate(path, sortOrder, database)
176
+ : createShardQueryHistorical(path, filters, sortOrder, database),
151
177
  database,
152
178
  action: queryAction,
153
179
  },
@@ -48,6 +48,9 @@ const paramSetup = {
48
48
  generalTab: {
49
49
  stateKey: 'tenant.diagnosticsTab',
50
50
  },
51
+ shardsMode: {
52
+ stateKey: 'shardsWorkload.filters.mode',
53
+ },
51
54
  shardsDateFrom: {
52
55
  stateKey: 'shardsWorkload.filters.from',
53
56
  type: 'number',
@@ -3,11 +3,17 @@ import type {ApiRequestAction} from '../../store/utils';
3
3
  import type {IResponseError} from '../api/error';
4
4
  import type {IQueryResult} from './query';
5
5
 
6
+ export enum EShardsWorkloadMode {
7
+ Immediate = 'immediate',
8
+ History = 'history',
9
+ }
10
+
6
11
  export interface IShardsWorkloadFilters {
7
12
  /** ms from epoch */
8
13
  from?: number;
9
14
  /** ms from epoch */
10
15
  to?: number;
16
+ mode?: EShardsWorkloadMode;
11
17
  }
12
18
 
13
19
  export interface IShardsWorkloadState {
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Intended to typecheck enums, but also checks if the value is any of the arbitrary object values
3
+ */
4
+ export const isEnumMember = <T extends Object>(object: T, value: any): value is T[keyof T] =>
5
+ Object.values(object).includes(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-embedded-ui",
3
- "version": "3.3.3",
3
+ "version": "3.3.4",
4
4
  "files": [
5
5
  "dist"
6
6
  ],
@@ -1,4 +0,0 @@
1
- {
2
- "no-data": "No data",
3
- "description": "Shards with CPU load over 70% are listed"
4
- }
@@ -1,4 +0,0 @@
1
- {
2
- "no-data": "Нет данных",
3
- "description": "Отображаются шарды с загрузкой CPU выше 70%"
4
- }
@@ -1 +0,0 @@
1
- export * from './OverloadedShards';