ydb-embedded-ui 4.30.0 → 4.31.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. package/README.md +2 -0
  2. package/dist/components/MetricChart/MetricChart.scss +34 -0
  3. package/dist/components/MetricChart/MetricChart.tsx +198 -0
  4. package/dist/components/MetricChart/convertReponse.ts +33 -0
  5. package/dist/components/MetricChart/getChartData.ts +20 -0
  6. package/dist/components/MetricChart/getDefaultDataFormatter.ts +45 -0
  7. package/dist/components/MetricChart/index.ts +2 -0
  8. package/dist/components/MetricChart/reducer.ts +86 -0
  9. package/dist/components/MetricChart/types.ts +32 -0
  10. package/dist/components/TimeFrameSelector/TimeFrameSelector.scss +5 -0
  11. package/dist/components/TimeFrameSelector/TimeFrameSelector.tsx +33 -0
  12. package/dist/containers/App/Content.js +16 -12
  13. package/dist/containers/Tenant/Diagnostics/TenantOverview/DefaultDashboard.tsx +50 -0
  14. package/dist/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx +13 -4
  15. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/CpuDashboard.tsx +18 -0
  16. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.tsx +2 -0
  17. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.scss +14 -0
  18. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.tsx +71 -0
  19. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemoryDashboard.tsx +21 -0
  20. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TenantMemory.tsx +7 -1
  21. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx +2 -1
  22. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/StorageDashboard.tsx +21 -0
  23. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.tsx +2 -0
  24. package/dist/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json +7 -1
  25. package/dist/containers/Tenant/Diagnostics/TenantOverview/i18n/ru.json +7 -1
  26. package/dist/containers/UserSettings/i18n/en.json +4 -1
  27. package/dist/containers/UserSettings/i18n/ru.json +4 -1
  28. package/dist/containers/UserSettings/settings.ts +12 -1
  29. package/dist/services/api.ts +18 -0
  30. package/dist/services/settings.ts +2 -0
  31. package/dist/store/reducers/tenant/tenant.ts +6 -1
  32. package/dist/types/api/render.ts +34 -0
  33. package/dist/utils/cn.ts +3 -0
  34. package/dist/utils/constants.ts +4 -0
  35. package/dist/utils/timeParsers/formatDuration.ts +10 -0
  36. package/dist/utils/timeframes.ts +10 -0
  37. package/dist/utils/versions/getVersionsColors.ts +1 -0
  38. package/package.json +3 -1
  39. package/CHANGELOG.md +0 -1559
package/README.md CHANGED
@@ -15,6 +15,8 @@ docker pull cr.yandex/yc/yandex-docker-local-ydb
15
15
  docker run -dp 8765:8765 cr.yandex/yc/yandex-docker-local-ydb
16
16
  ```
17
17
 
18
+ Open http://localhost:8765 to view it in the browser.
19
+
18
20
  ## Development
19
21
 
20
22
  1. Run on a machine with Docker installed:
@@ -0,0 +1,34 @@
1
+ .ydb-metric-chart {
2
+ display: flex;
3
+ flex-direction: column;
4
+
5
+ padding: 16px 16px 8px;
6
+
7
+ border: 1px solid var(--g-color-line-generic);
8
+ border-radius: 8px;
9
+
10
+ &__title {
11
+ margin-bottom: 10px;
12
+ }
13
+
14
+ &__chart {
15
+ position: relative;
16
+
17
+ display: flex;
18
+ overflow: hidden;
19
+
20
+ width: 100%;
21
+ height: 100%;
22
+ }
23
+
24
+ &__error {
25
+ position: absolute;
26
+ z-index: 1;
27
+ top: 10%;
28
+ left: 50%;
29
+
30
+ text-align: center;
31
+
32
+ transform: translateX(-50%);
33
+ }
34
+ }
@@ -0,0 +1,198 @@
1
+ import {useCallback, useEffect, useReducer, useRef} from 'react';
2
+
3
+ import {RawSerieData, YagrPlugin, YagrWidgetData} from '@gravity-ui/chartkit/yagr';
4
+ import ChartKit, {settings} from '@gravity-ui/chartkit';
5
+
6
+ import type {IResponseError} from '../../types/api/error';
7
+ import type {TimeFrame} from '../../utils/timeframes';
8
+ import {useAutofetcher} from '../../utils/hooks';
9
+ import {COLORS} from '../../utils/versions';
10
+ import {cn} from '../../utils/cn';
11
+
12
+ import {Loader} from '../Loader';
13
+ import {ResponseError} from '../Errors/ResponseError';
14
+
15
+ import type {ChartOptions, MetricDescription, PreparedMetricsData} from './types';
16
+ import {convertResponse} from './convertReponse';
17
+ import {getDefaultDataFormatter} from './getDefaultDataFormatter';
18
+ import {getChartData} from './getChartData';
19
+ import {
20
+ chartReducer,
21
+ initialChartState,
22
+ setChartData,
23
+ setChartDataLoading,
24
+ setChartDataWasNotLoaded,
25
+ setChartError,
26
+ } from './reducer';
27
+
28
+ import './MetricChart.scss';
29
+
30
+ const b = cn('ydb-metric-chart');
31
+
32
+ settings.set({plugins: [YagrPlugin]});
33
+
34
+ const prepareWidgetData = (
35
+ data: PreparedMetricsData,
36
+ options: ChartOptions = {},
37
+ ): YagrWidgetData => {
38
+ const {dataType} = options;
39
+ const defaultDataFormatter = getDefaultDataFormatter(dataType);
40
+
41
+ const isDataEmpty = !data.metrics.length;
42
+
43
+ const graphs: RawSerieData[] = data.metrics.map((metric, index) => {
44
+ return {
45
+ id: metric.target,
46
+ name: metric.title || metric.target,
47
+ color: metric.color || COLORS[index],
48
+ data: metric.data,
49
+ formatter: defaultDataFormatter,
50
+ };
51
+ });
52
+
53
+ return {
54
+ data: {
55
+ timeline: data.timeline,
56
+ graphs,
57
+ },
58
+
59
+ libraryConfig: {
60
+ chart: {
61
+ size: {
62
+ // When empty data chart is displayed without axes it have different paddings
63
+ // To compensate it, additional paddings are applied
64
+ padding: isDataEmpty ? [10, 0, 10, 0] : undefined,
65
+ },
66
+ series: {
67
+ type: 'line',
68
+ },
69
+ select: {
70
+ zoom: false,
71
+ },
72
+ },
73
+ scales: {
74
+ y: {
75
+ type: 'linear',
76
+ range: 'nice',
77
+ },
78
+ },
79
+ axes: {
80
+ y: {
81
+ values: defaultDataFormatter
82
+ ? (_, ticks) => ticks.map(defaultDataFormatter)
83
+ : undefined,
84
+ },
85
+ },
86
+ tooltip: {
87
+ show: true,
88
+ tracking: 'sticky',
89
+ },
90
+ },
91
+ };
92
+ };
93
+
94
+ interface DiagnosticsChartProps {
95
+ title?: string;
96
+ metrics: MetricDescription[];
97
+ timeFrame?: TimeFrame;
98
+
99
+ autorefresh?: boolean;
100
+
101
+ height?: number;
102
+ width?: number;
103
+
104
+ chartOptions?: ChartOptions;
105
+ }
106
+
107
+ export const MetricChart = ({
108
+ title,
109
+ metrics,
110
+ timeFrame = '1h',
111
+ autorefresh,
112
+ width = 400,
113
+ height = width / 1.5,
114
+ chartOptions,
115
+ }: DiagnosticsChartProps) => {
116
+ const mounted = useRef(false);
117
+
118
+ useEffect(() => {
119
+ mounted.current = true;
120
+ return () => {
121
+ mounted.current = false;
122
+ };
123
+ }, []);
124
+
125
+ const [{loading, wasLoaded, data, error}, dispatch] = useReducer(
126
+ chartReducer,
127
+ initialChartState,
128
+ );
129
+
130
+ const fetchChartData = useCallback(
131
+ async (isBackground: boolean) => {
132
+ dispatch(setChartDataLoading());
133
+
134
+ if (!isBackground) {
135
+ dispatch(setChartDataWasNotLoaded());
136
+ }
137
+
138
+ try {
139
+ // maxDataPoints param is calculated based on width
140
+ // should be width > maxDataPoints to prevent points that cannot be selected
141
+ // more px per dataPoint - easier to select, less - chart is smoother
142
+ const response = await getChartData({
143
+ metrics,
144
+ timeFrame,
145
+ maxDataPoints: width / 2,
146
+ });
147
+
148
+ // Hack to prevent setting value to state, if component unmounted
149
+ if (!mounted.current) return;
150
+
151
+ // In some cases error could be in response with 200 status code
152
+ // It happens when request is OK, but chart data cannot be returned due to some reason
153
+ // Example: charts are not enabled in the DB ('GraphShard is not enabled' error)
154
+ if (Array.isArray(response)) {
155
+ const preparedData = convertResponse(response, metrics);
156
+ dispatch(setChartData(preparedData));
157
+ } else {
158
+ dispatch(setChartError({statusText: response.error}));
159
+ }
160
+ } catch (err) {
161
+ if (!mounted.current) return;
162
+
163
+ dispatch(setChartError(err as IResponseError));
164
+ }
165
+ },
166
+ [metrics, timeFrame, width],
167
+ );
168
+
169
+ useAutofetcher(fetchChartData, [fetchChartData], autorefresh);
170
+
171
+ const convertedData = prepareWidgetData(data, chartOptions);
172
+
173
+ const renderContent = () => {
174
+ if (loading && !wasLoaded) {
175
+ return <Loader />;
176
+ }
177
+
178
+ return (
179
+ <div className={b('chart')}>
180
+ <ChartKit type="yagr" data={convertedData} />
181
+ {error && <ResponseError className={b('error')} error={error} />}
182
+ </div>
183
+ );
184
+ };
185
+
186
+ return (
187
+ <div
188
+ className={b(null)}
189
+ style={{
190
+ height,
191
+ width,
192
+ }}
193
+ >
194
+ <div className={b('title')}>{title}</div>
195
+ {renderContent()}
196
+ </div>
197
+ );
198
+ };
@@ -0,0 +1,33 @@
1
+ import type {MetricData} from '../../types/api/render';
2
+ import type {MetricDescription, PreparedMetric, PreparedMetricsData} from './types';
3
+
4
+ export const convertResponse = (
5
+ data: MetricData[] = [],
6
+ metrics: MetricDescription[],
7
+ ): PreparedMetricsData => {
8
+ const preparedMetrics = data
9
+ .map(({datapoints, target}) => {
10
+ const metricDescription = metrics.find((metric) => metric.target === target);
11
+
12
+ if (!metricDescription) {
13
+ return undefined;
14
+ }
15
+
16
+ const chartData = datapoints.map((datapoint) => datapoint[0]);
17
+
18
+ return {
19
+ ...metricDescription,
20
+ data: chartData,
21
+ };
22
+ })
23
+ .filter((metric): metric is PreparedMetric => metric !== undefined);
24
+
25
+ // Asuming all metrics in response have the same timeline
26
+ // Backend return data in seconds, while chart needs ms
27
+ const timeline = data[0].datapoints.map((datapoint) => datapoint[1] * 1000);
28
+
29
+ return {
30
+ timeline,
31
+ metrics: preparedMetrics,
32
+ };
33
+ };
@@ -0,0 +1,20 @@
1
+ import {TIMEFRAMES, type TimeFrame} from '../../utils/timeframes';
2
+ import type {MetricDescription} from './types';
3
+
4
+ interface GetChartDataParams {
5
+ metrics: MetricDescription[];
6
+ timeFrame: TimeFrame;
7
+ maxDataPoints: number;
8
+ }
9
+
10
+ export const getChartData = async ({metrics, timeFrame, maxDataPoints}: GetChartDataParams) => {
11
+ const targetString = metrics.map((metric) => `target=${metric.target}`).join('&');
12
+
13
+ const until = Math.round(Date.now() / 1000);
14
+ const from = until - TIMEFRAMES[timeFrame];
15
+
16
+ return window.api.getChartData(
17
+ {target: targetString, from, until, maxDataPoints},
18
+ {concurrentId: `getChartData|${targetString}`},
19
+ );
20
+ };
@@ -0,0 +1,45 @@
1
+ import {formatBytes} from '../../utils/bytesParsers';
2
+ import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants';
3
+ import {roundToPrecision} from '../../utils/dataFormatters/dataFormatters';
4
+ import {formatToMs} from '../../utils/timeParsers';
5
+ import {isNumeric} from '../../utils/utils';
6
+
7
+ import type {ChartDataType, ChartValue} from './types';
8
+
9
+ export const getDefaultDataFormatter = (dataType?: ChartDataType) => {
10
+ switch (dataType) {
11
+ case 'ms': {
12
+ return formatChartValueToMs;
13
+ }
14
+ case 'size': {
15
+ return formatChartValueToSize;
16
+ }
17
+ default:
18
+ return undefined;
19
+ }
20
+ };
21
+
22
+ // Values in y axis won't be null and will always be present and properly formatted
23
+ // EMPTY_DATA_PLACEHOLDER is actually empty data format for values in a tooltip
24
+ function formatChartValueToMs(value: ChartValue) {
25
+ if (value === null) {
26
+ return EMPTY_DATA_PLACEHOLDER;
27
+ }
28
+ return formatToMs(roundToPrecision(convertToNumber(value), 2));
29
+ }
30
+
31
+ function formatChartValueToSize(value: ChartValue) {
32
+ if (value === null) {
33
+ return EMPTY_DATA_PLACEHOLDER;
34
+ }
35
+ return formatBytes({value: convertToNumber(value), precision: 3});
36
+ }
37
+
38
+ // Numeric values expected, not numeric value should be displayd as 0
39
+ function convertToNumber(value: unknown): number {
40
+ if (isNumeric(value)) {
41
+ return Number(value);
42
+ }
43
+
44
+ return 0;
45
+ }
@@ -0,0 +1,2 @@
1
+ export type {MetricDescription, Metric, ChartOptions} from './types';
2
+ export {MetricChart} from './MetricChart';
@@ -0,0 +1,86 @@
1
+ import {createRequestActionTypes} from '../../store/utils';
2
+ import type {IResponseError} from '../../types/api/error';
3
+
4
+ import type {PreparedMetricsData} from './types';
5
+
6
+ const FETCH_CHART_DATA = createRequestActionTypes('chart', 'FETCH_CHART_DATA');
7
+ const SET_CHART_DATA_WAS_NOT_LOADED = 'chart/SET_DATA_WAS_NOT_LOADED';
8
+
9
+ export const setChartDataLoading = () => {
10
+ return {
11
+ type: FETCH_CHART_DATA.REQUEST,
12
+ } as const;
13
+ };
14
+
15
+ export const setChartData = (data: PreparedMetricsData) => {
16
+ return {
17
+ data,
18
+ type: FETCH_CHART_DATA.SUCCESS,
19
+ } as const;
20
+ };
21
+
22
+ export const setChartError = (error: IResponseError) => {
23
+ return {
24
+ error,
25
+ type: FETCH_CHART_DATA.FAILURE,
26
+ } as const;
27
+ };
28
+
29
+ export const setChartDataWasNotLoaded = () => {
30
+ return {
31
+ type: SET_CHART_DATA_WAS_NOT_LOADED,
32
+ } as const;
33
+ };
34
+
35
+ type ChartAction =
36
+ | ReturnType<typeof setChartDataLoading>
37
+ | ReturnType<typeof setChartData>
38
+ | ReturnType<typeof setChartError>
39
+ | ReturnType<typeof setChartDataWasNotLoaded>;
40
+
41
+ interface ChartState {
42
+ loading: boolean;
43
+ wasLoaded: boolean;
44
+ data: PreparedMetricsData;
45
+ error: IResponseError | undefined;
46
+ }
47
+
48
+ export const initialChartState: ChartState = {
49
+ // Set chart initial state as loading, in order not to mount and unmount component in between requests
50
+ // as it leads to memory leak errors in console (not proper useEffect cleanups in chart component itself)
51
+ // TODO: possible fix (check needed): chart component is always present, but display: none for chart while loading
52
+ loading: true,
53
+ wasLoaded: false,
54
+ data: {timeline: [], metrics: []},
55
+ error: undefined,
56
+ };
57
+
58
+ export const chartReducer = (state: ChartState, action: ChartAction) => {
59
+ switch (action.type) {
60
+ case FETCH_CHART_DATA.REQUEST: {
61
+ return {...state, loading: true};
62
+ }
63
+ case FETCH_CHART_DATA.SUCCESS: {
64
+ return {...state, loading: false, wasLoaded: true, error: undefined, data: action.data};
65
+ }
66
+ case FETCH_CHART_DATA.FAILURE: {
67
+ if (action.error?.isCancelled) {
68
+ return state;
69
+ }
70
+
71
+ return {
72
+ ...state,
73
+ error: action.error,
74
+ // Clear data, so error will be displayed with empty chart
75
+ data: {timeline: [], metrics: []},
76
+ loading: false,
77
+ wasLoaded: true,
78
+ };
79
+ }
80
+ case SET_CHART_DATA_WAS_NOT_LOADED: {
81
+ return {...state, wasLoaded: false};
82
+ }
83
+ default:
84
+ return state;
85
+ }
86
+ };
@@ -0,0 +1,32 @@
1
+ export type Metric =
2
+ | 'queries.requests'
3
+ | 'queries.latencies.p50'
4
+ | 'queries.latencies.p75'
5
+ | 'queries.latencies.p90'
6
+ | 'queries.latencies.p99'
7
+ | 'resources.cpu.usage'
8
+ | 'resources.memory.used_bytes'
9
+ | 'resources.storage.used_bytes';
10
+
11
+ export interface MetricDescription {
12
+ target: Metric;
13
+ title?: string;
14
+ color?: string;
15
+ }
16
+
17
+ export interface PreparedMetric extends MetricDescription {
18
+ data: (number | null)[];
19
+ }
20
+
21
+ export interface PreparedMetricsData {
22
+ timeline: number[];
23
+ metrics: PreparedMetric[];
24
+ }
25
+
26
+ export type ChartValue = number | string | null;
27
+
28
+ export type ChartDataType = 'ms' | 'size';
29
+
30
+ export interface ChartOptions {
31
+ dataType?: ChartDataType;
32
+ }
@@ -0,0 +1,5 @@
1
+ .ydb-timeframe-selector {
2
+ display: flex;
3
+
4
+ gap: 2px;
5
+ }
@@ -0,0 +1,33 @@
1
+ import {Button} from '@gravity-ui/uikit';
2
+
3
+ import {cn} from '../../utils/cn';
4
+ import {TIMEFRAMES, type TimeFrame} from '../../utils/timeframes';
5
+
6
+ import './TimeFrameSelector.scss';
7
+
8
+ const b = cn('ydb-timeframe-selector');
9
+
10
+ interface TimeFrameSelectorProps {
11
+ value: TimeFrame;
12
+ onChange: (value: TimeFrame) => void;
13
+ className?: string;
14
+ }
15
+
16
+ export const TimeFrameSelector = ({value, onChange, className}: TimeFrameSelectorProps) => {
17
+ return (
18
+ <div className={b(null, className)}>
19
+ {Object.keys(TIMEFRAMES).map((timeFrame) => {
20
+ return (
21
+ <Button
22
+ view="flat"
23
+ selected={value === timeFrame}
24
+ key={timeFrame}
25
+ onClick={() => onChange(timeFrame as TimeFrame)}
26
+ >
27
+ {timeFrame}
28
+ </Button>
29
+ );
30
+ })}
31
+ </div>
32
+ );
33
+ };
@@ -1,5 +1,7 @@
1
1
  import React from 'react';
2
2
  import {Switch, Route, Redirect, Router, useLocation} from 'react-router-dom';
3
+ import {QueryParamProvider} from 'use-query-params';
4
+ import {ReactRouter5Adapter} from 'use-query-params/adapters/react-router-5';
3
5
  import cn from 'bem-cn-lite';
4
6
  import {connect} from 'react-redux';
5
7
 
@@ -80,18 +82,20 @@ function ContentWrapper(props) {
80
82
  <HistoryContext.Consumer>
81
83
  {(history) => (
82
84
  <Router history={history}>
83
- <Switch>
84
- <Route path={routes.auth}>
85
- <Authentication closable />
86
- </Route>
87
- <Route>
88
- <ThemeProvider theme={theme}>
89
- <div className={b({embedded: singleClusterMode})}>
90
- {isAuthenticated ? props.children : <Authentication />}
91
- </div>
92
- </ThemeProvider>
93
- </Route>
94
- </Switch>
85
+ <QueryParamProvider adapter={ReactRouter5Adapter}>
86
+ <Switch>
87
+ <Route path={routes.auth}>
88
+ <Authentication closable />
89
+ </Route>
90
+ <Route>
91
+ <ThemeProvider theme={theme}>
92
+ <div className={b({embedded: singleClusterMode})}>
93
+ {isAuthenticated ? props.children : <Authentication />}
94
+ </div>
95
+ </ThemeProvider>
96
+ </Route>
97
+ </Switch>
98
+ </QueryParamProvider>
95
99
  </Router>
96
100
  )}
97
101
  </HistoryContext.Consumer>
@@ -0,0 +1,50 @@
1
+ import {type ChartConfig, TenantDashboard} from './TenantDashboard/TenantDashboard';
2
+ import i18n from './i18n';
3
+
4
+ const defaultDashboardConfig: ChartConfig[] = [
5
+ {
6
+ title: i18n('charts.queries-per-second'),
7
+ metrics: [
8
+ {
9
+ target: 'queries.requests',
10
+ title: i18n('charts.queries-per-second'),
11
+ },
12
+ ],
13
+ },
14
+ {
15
+ title: i18n('charts.transaction-latency', {percentile: ''}),
16
+ metrics: [
17
+ {
18
+ target: 'queries.latencies.p50',
19
+ title: i18n('charts.transaction-latency', {
20
+ percentile: 'p50',
21
+ }),
22
+ },
23
+ {
24
+ target: 'queries.latencies.p75',
25
+ title: i18n('charts.transaction-latency', {
26
+ percentile: 'p75',
27
+ }),
28
+ },
29
+ {
30
+ target: 'queries.latencies.p90',
31
+ title: i18n('charts.transaction-latency', {
32
+ percentile: 'p90',
33
+ }),
34
+ },
35
+ {
36
+ target: 'queries.latencies.p99',
37
+ title: i18n('charts.transaction-latency', {
38
+ percentile: 'p99',
39
+ }),
40
+ },
41
+ ],
42
+ options: {
43
+ dataType: 'ms',
44
+ },
45
+ },
46
+ ];
47
+
48
+ export const DefaultDashboard = () => {
49
+ return <TenantDashboard charts={defaultDashboardConfig} />;
50
+ };
@@ -72,22 +72,31 @@ export function MetricsCards({
72
72
 
73
73
  const queryParams = parseQuery(location);
74
74
 
75
+ // Allow tabs untoggle behaviour
76
+ const getTabIfNotActive = (tab: TenantMetricsTab) => {
77
+ if (tab === metricsTab) {
78
+ return '';
79
+ }
80
+
81
+ return tab;
82
+ };
83
+
75
84
  const tabLinks: Record<TenantMetricsTab, string> = {
76
85
  [TENANT_METRICS_TABS_IDS.cpu]: getTenantPath({
77
86
  ...queryParams,
78
- [TenantTabsGroups.metricsTab]: TENANT_METRICS_TABS_IDS.cpu,
87
+ [TenantTabsGroups.metricsTab]: getTabIfNotActive(TENANT_METRICS_TABS_IDS.cpu),
79
88
  }),
80
89
  [TENANT_METRICS_TABS_IDS.storage]: getTenantPath({
81
90
  ...queryParams,
82
- [TenantTabsGroups.metricsTab]: TENANT_METRICS_TABS_IDS.storage,
91
+ [TenantTabsGroups.metricsTab]: getTabIfNotActive(TENANT_METRICS_TABS_IDS.storage),
83
92
  }),
84
93
  [TENANT_METRICS_TABS_IDS.memory]: getTenantPath({
85
94
  ...queryParams,
86
- [TenantTabsGroups.metricsTab]: TENANT_METRICS_TABS_IDS.memory,
95
+ [TenantTabsGroups.metricsTab]: getTabIfNotActive(TENANT_METRICS_TABS_IDS.memory),
87
96
  }),
88
97
  [TENANT_METRICS_TABS_IDS.healthcheck]: getTenantPath({
89
98
  ...queryParams,
90
- [TenantTabsGroups.metricsTab]: TENANT_METRICS_TABS_IDS.healthcheck,
99
+ [TenantTabsGroups.metricsTab]: getTabIfNotActive(TENANT_METRICS_TABS_IDS.healthcheck),
91
100
  }),
92
101
  };
93
102
 
@@ -0,0 +1,18 @@
1
+ import {type ChartConfig, TenantDashboard} from '../TenantDashboard/TenantDashboard';
2
+ import i18n from '../i18n';
3
+
4
+ const cpuDashboardConfig: ChartConfig[] = [
5
+ {
6
+ title: i18n('charts.cpu-usage'),
7
+ metrics: [
8
+ {
9
+ target: 'resources.cpu.usage',
10
+ title: i18n('charts.cpu-usage'),
11
+ },
12
+ ],
13
+ },
14
+ ];
15
+
16
+ export const CpuDashboard = () => {
17
+ return <TenantDashboard charts={cpuDashboardConfig} />;
18
+ };
@@ -1,4 +1,5 @@
1
1
  import type {AdditionalNodesProps} from '../../../../../types/additionalProps';
2
+ import {CpuDashboard} from './CpuDashboard';
2
3
  import {TopNodesByLoad} from './TopNodesByLoad';
3
4
  import {TopNodesByCpu} from './TopNodesByCpu';
4
5
  import {TopShards} from './TopShards';
@@ -12,6 +13,7 @@ interface TenantCpuProps {
12
13
  export function TenantCpu({path, additionalNodesProps}: TenantCpuProps) {
13
14
  return (
14
15
  <>
16
+ <CpuDashboard />
15
17
  <TopNodesByLoad path={path} additionalNodesProps={additionalNodesProps} />
16
18
  <TopNodesByCpu path={path} additionalNodesProps={additionalNodesProps} />
17
19
  <TopShards path={path} />
@@ -0,0 +1,14 @@
1
+ .ydb-tenant-dashboard {
2
+ width: var(--diagnostics-section-table-width);
3
+ margin-bottom: var(--diagnostics-section-margin);
4
+
5
+ &__controls {
6
+ margin-bottom: 10px;
7
+ }
8
+
9
+ &__charts {
10
+ display: flex;
11
+ flex-flow: row wrap;
12
+ gap: 16px;
13
+ }
14
+ }