ydb-embedded-ui 4.30.0 → 4.31.0
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/components/MetricChart/MetricChart.scss +34 -0
- package/dist/components/MetricChart/MetricChart.tsx +198 -0
- package/dist/components/MetricChart/convertReponse.ts +32 -0
- package/dist/components/MetricChart/getChartData.ts +20 -0
- package/dist/components/MetricChart/getDefaultDataFormatter.ts +36 -0
- package/dist/components/MetricChart/index.ts +2 -0
- package/dist/components/MetricChart/reducer.ts +86 -0
- package/dist/components/MetricChart/types.ts +32 -0
- package/dist/components/TimeFrameSelector/TimeFrameSelector.scss +5 -0
- package/dist/components/TimeFrameSelector/TimeFrameSelector.tsx +33 -0
- package/dist/containers/App/Content.js +16 -12
- package/dist/containers/Tenant/Diagnostics/TenantOverview/DefaultDashboard.tsx +50 -0
- package/dist/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx +13 -4
- package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/CpuDashboard.tsx +18 -0
- package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.tsx +2 -0
- package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.scss +14 -0
- package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.tsx +71 -0
- package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemoryDashboard.tsx +21 -0
- package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TenantMemory.tsx +7 -1
- package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx +2 -1
- package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/StorageDashboard.tsx +21 -0
- package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.tsx +2 -0
- package/dist/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json +7 -1
- package/dist/containers/Tenant/Diagnostics/TenantOverview/i18n/ru.json +7 -1
- package/dist/containers/UserSettings/i18n/en.json +4 -1
- package/dist/containers/UserSettings/i18n/ru.json +4 -1
- package/dist/containers/UserSettings/settings.ts +12 -1
- package/dist/services/api.ts +18 -0
- package/dist/services/settings.ts +2 -0
- package/dist/store/reducers/tenant/tenant.ts +6 -1
- package/dist/types/api/render.ts +34 -0
- package/dist/utils/cn.ts +3 -0
- package/dist/utils/constants.ts +2 -0
- package/dist/utils/timeParsers/formatDuration.ts +10 -0
- package/dist/utils/timeframes.ts +10 -0
- package/dist/utils/versions/getVersionsColors.ts +1 -0
- package/package.json +3 -1
- 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,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,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
|
-
<
|
84
|
-
<
|
85
|
-
<
|
86
|
-
|
87
|
-
|
88
|
-
<
|
89
|
-
<
|
90
|
-
{
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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} />
|