ydb-embedded-ui 4.30.0 → 4.31.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 (38) hide show
  1. package/dist/components/MetricChart/MetricChart.scss +34 -0
  2. package/dist/components/MetricChart/MetricChart.tsx +198 -0
  3. package/dist/components/MetricChart/convertReponse.ts +32 -0
  4. package/dist/components/MetricChart/getChartData.ts +20 -0
  5. package/dist/components/MetricChart/getDefaultDataFormatter.ts +36 -0
  6. package/dist/components/MetricChart/index.ts +2 -0
  7. package/dist/components/MetricChart/reducer.ts +86 -0
  8. package/dist/components/MetricChart/types.ts +32 -0
  9. package/dist/components/TimeFrameSelector/TimeFrameSelector.scss +5 -0
  10. package/dist/components/TimeFrameSelector/TimeFrameSelector.tsx +33 -0
  11. package/dist/containers/App/Content.js +16 -12
  12. package/dist/containers/Tenant/Diagnostics/TenantOverview/DefaultDashboard.tsx +50 -0
  13. package/dist/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx +13 -4
  14. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/CpuDashboard.tsx +18 -0
  15. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.tsx +2 -0
  16. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.scss +14 -0
  17. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.tsx +71 -0
  18. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemoryDashboard.tsx +21 -0
  19. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TenantMemory.tsx +7 -1
  20. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx +2 -1
  21. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/StorageDashboard.tsx +21 -0
  22. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.tsx +2 -0
  23. package/dist/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json +7 -1
  24. package/dist/containers/Tenant/Diagnostics/TenantOverview/i18n/ru.json +7 -1
  25. package/dist/containers/UserSettings/i18n/en.json +4 -1
  26. package/dist/containers/UserSettings/i18n/ru.json +4 -1
  27. package/dist/containers/UserSettings/settings.ts +12 -1
  28. package/dist/services/api.ts +18 -0
  29. package/dist/services/settings.ts +2 -0
  30. package/dist/store/reducers/tenant/tenant.ts +6 -1
  31. package/dist/types/api/render.ts +34 -0
  32. package/dist/utils/cn.ts +3 -0
  33. package/dist/utils/constants.ts +2 -0
  34. package/dist/utils/timeParsers/formatDuration.ts +10 -0
  35. package/dist/utils/timeframes.ts +10 -0
  36. package/dist/utils/versions/getVersionsColors.ts +1 -0
  37. package/package.json +3 -1
  38. package/CHANGELOG.md +0 -1559
@@ -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,32 @@
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
+ const chartData = datapoints.map((datapoint) => datapoint[0] || 0);
12
+
13
+ if (!metricDescription) {
14
+ return undefined;
15
+ }
16
+
17
+ return {
18
+ ...metricDescription,
19
+ data: chartData,
20
+ };
21
+ })
22
+ .filter((metric): metric is PreparedMetric => metric !== undefined);
23
+
24
+ // Asuming all metrics in response have the same timeline
25
+ // Backend return data in seconds, while chart needs ms
26
+ const timeline = data[0].datapoints.map((datapoint) => datapoint[1] * 1000);
27
+
28
+ return {
29
+ timeline,
30
+ metrics: preparedMetrics,
31
+ };
32
+ };
@@ -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,36 @@
1
+ import {formatBytes} from '../../utils/bytesParsers';
2
+ import {roundToPrecision} from '../../utils/dataFormatters/dataFormatters';
3
+ import {formatToMs} from '../../utils/timeParsers';
4
+ import {isNumeric} from '../../utils/utils';
5
+
6
+ import type {ChartDataType, ChartValue} from './types';
7
+
8
+ export const getDefaultDataFormatter = (dataType?: ChartDataType) => {
9
+ switch (dataType) {
10
+ case 'ms': {
11
+ return formatChartValueToMs;
12
+ }
13
+ case 'size': {
14
+ return formatChartValueToSize;
15
+ }
16
+ default:
17
+ return undefined;
18
+ }
19
+ };
20
+
21
+ function formatChartValueToMs(value: ChartValue) {
22
+ return formatToMs(roundToPrecision(convertToNumber(value), 2));
23
+ }
24
+
25
+ function formatChartValueToSize(value: ChartValue) {
26
+ return formatBytes({value: convertToNumber(value), precision: 3});
27
+ }
28
+
29
+ // Numeric values expected, not numeric value should be displayd as 0
30
+ function convertToNumber(value: unknown): number {
31
+ if (isNumeric(value)) {
32
+ return Number(value);
33
+ }
34
+
35
+ return 0;
36
+ }
@@ -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[];
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
+ }