ydb-embedded-ui 4.29.0 → 4.31.0
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +2 -1
- package/dist/components/ClipboardButton/ClipboardButton.tsx +52 -0
- package/dist/components/ClipboardButton/index.ts +1 -0
- package/dist/components/EntityStatus/EntityStatus.js +9 -12
- package/dist/components/EntityStatus/EntityStatus.scss +2 -13
- 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/App.scss +9 -9
- 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/Tenant/ObjectSummary/ObjectSummary.tsx +24 -30
- package/dist/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx +10 -16
- 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/containers/Versions/NodesTreeTitle/NodesTreeTitle.scss +15 -0
- package/dist/containers/Versions/NodesTreeTitle/NodesTreeTitle.tsx +9 -6
- 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 +4 -2
- package/CHANGELOG.md +0 -1552
- package/dist/components/CopyToClipboard/CopyToClipboard.tsx +0 -38
package/README.md
CHANGED
@@ -2,7 +2,8 @@
|
|
2
2
|
|
3
3
|
Local viewer for YDB clusters
|
4
4
|
|
5
|
-
[Docs for users](https://ydb.tech/en/docs/maintenance/embedded_monitoring/ydb_monitoring)
|
5
|
+
* [Docs for users](https://ydb.tech/en/docs/maintenance/embedded_monitoring/ydb_monitoring)
|
6
|
+
* [Project Roadmap](ROADMAP.md)
|
6
7
|
|
7
8
|
## Preview
|
8
9
|
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import {
|
2
|
+
Button,
|
3
|
+
ButtonProps,
|
4
|
+
ClipboardIcon,
|
5
|
+
CopyToClipboard as CopyToClipboardUiKit,
|
6
|
+
CopyToClipboardStatus,
|
7
|
+
Tooltip,
|
8
|
+
} from '@gravity-ui/uikit';
|
9
|
+
import cn from 'bem-cn-lite';
|
10
|
+
|
11
|
+
const b = cn('clipboard-button');
|
12
|
+
|
13
|
+
interface ClipboardButtonProps extends Pick<ButtonProps, 'disabled' | 'size' | 'title' | 'view'> {
|
14
|
+
className?: string;
|
15
|
+
text: string;
|
16
|
+
}
|
17
|
+
|
18
|
+
/**
|
19
|
+
* An inner component required
|
20
|
+
* because `react-copy-to-clipboard` doesn't work with `Tooltip` otherwise.
|
21
|
+
*/
|
22
|
+
function InnerButton({
|
23
|
+
className,
|
24
|
+
status,
|
25
|
+
title,
|
26
|
+
...props
|
27
|
+
}: Omit<ClipboardButtonProps, 'text'> & {status: CopyToClipboardStatus}) {
|
28
|
+
return (
|
29
|
+
<Tooltip
|
30
|
+
content={status === CopyToClipboardStatus.Success ? 'Copied!' : title || 'Copy'}
|
31
|
+
/**
|
32
|
+
* Auto-placement has a bug with text changing.
|
33
|
+
* @link https://github.com/ydb-platform/ydb-embedded-ui/pull/648#discussion_r1453530092
|
34
|
+
*/
|
35
|
+
placement="bottom-start"
|
36
|
+
>
|
37
|
+
<Button {...props} className={b(null, className)}>
|
38
|
+
<Button.Icon>
|
39
|
+
<ClipboardIcon status={status} size={16} />
|
40
|
+
</Button.Icon>
|
41
|
+
</Button>
|
42
|
+
</Tooltip>
|
43
|
+
);
|
44
|
+
}
|
45
|
+
|
46
|
+
export function ClipboardButton({text, ...props}: ClipboardButtonProps) {
|
47
|
+
return (
|
48
|
+
<CopyToClipboardUiKit text={text} timeout={1000}>
|
49
|
+
{(status) => <InnerButton {...props} status={status} />}
|
50
|
+
</CopyToClipboardUiKit>
|
51
|
+
);
|
52
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from './ClipboardButton';
|
@@ -1,14 +1,13 @@
|
|
1
|
-
import
|
2
|
-
import PropTypes from 'prop-types';
|
1
|
+
import {Icon, Link as UIKitLink} from '@gravity-ui/uikit';
|
3
2
|
import cn from 'bem-cn-lite';
|
3
|
+
import PropTypes from 'prop-types';
|
4
|
+
import React from 'react';
|
4
5
|
import {Link} from 'react-router-dom';
|
5
|
-
import {ClipboardButton, Link as UIKitLink, Button, Icon} from '@gravity-ui/uikit';
|
6
|
-
|
7
|
-
import circleInfoIcon from '../../assets/icons/circle-info.svg';
|
8
6
|
import circleExclamationIcon from '../../assets/icons/circle-exclamation.svg';
|
9
|
-
import
|
7
|
+
import circleInfoIcon from '../../assets/icons/circle-info.svg';
|
10
8
|
import circleTimesIcon from '../../assets/icons/circle-xmark.svg';
|
11
|
-
|
9
|
+
import triangleExclamationIcon from '../../assets/icons/triangle-exclamation.svg';
|
10
|
+
import {ClipboardButton} from '../ClipboardButton';
|
12
11
|
import './EntityStatus.scss';
|
13
12
|
|
14
13
|
const icons = {
|
@@ -121,15 +120,13 @@ class EntityStatus extends React.Component {
|
|
121
120
|
{this.renderLink()}
|
122
121
|
</span>
|
123
122
|
{hasClipboardButton && (
|
124
|
-
<
|
125
|
-
|
123
|
+
<ClipboardButton
|
124
|
+
text={name}
|
126
125
|
size="s"
|
127
126
|
className={b('clipboard-button', {
|
128
127
|
visible: this.props.clipboardButtonAlwaysVisible,
|
129
128
|
})}
|
130
|
-
|
131
|
-
<ClipboardButton text={name} size={16} />
|
132
|
-
</Button>
|
129
|
+
/>
|
133
130
|
)}
|
134
131
|
</div>
|
135
132
|
);
|
@@ -10,25 +10,14 @@
|
|
10
10
|
@include body-2-typography();
|
11
11
|
|
12
12
|
&__clipboard-button {
|
13
|
-
|
14
|
-
justify-content: center;
|
15
|
-
align-items: center;
|
13
|
+
visibility: hidden;
|
16
14
|
|
17
15
|
margin-left: 8px;
|
18
16
|
|
19
17
|
color: var(--yc-color-text-secondary);
|
20
18
|
|
21
|
-
.yc-button__text {
|
22
|
-
margin: 0;
|
23
|
-
}
|
24
|
-
|
25
|
-
.yc-clipboard-button {
|
26
|
-
width: 24px;
|
27
|
-
height: 24px;
|
28
|
-
}
|
29
|
-
|
30
19
|
&_visible {
|
31
|
-
|
20
|
+
visibility: visible;
|
32
21
|
}
|
33
22
|
}
|
34
23
|
|
@@ -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
|
+
};
|
@@ -107,21 +107,21 @@ body,
|
|
107
107
|
border-right: unset;
|
108
108
|
border-left: unset;
|
109
109
|
}
|
110
|
-
|
111
|
-
.yc-clipboard-button {
|
112
|
-
display: inline-flex;
|
113
|
-
justify-content: center;
|
114
|
-
align-items: center;
|
115
|
-
}
|
116
110
|
}
|
117
111
|
|
118
112
|
.error {
|
119
113
|
color: var(--g-color-text-danger);
|
120
114
|
}
|
121
115
|
|
122
|
-
.data-table__row
|
123
|
-
.ydb-virtual-table__row
|
124
|
-
|
116
|
+
.data-table__row,
|
117
|
+
.ydb-virtual-table__row,
|
118
|
+
.ydb-tree-view__item {
|
119
|
+
&:hover,
|
120
|
+
&:focus-within {
|
121
|
+
& .clipboard-button {
|
122
|
+
visibility: visible;
|
123
|
+
}
|
124
|
+
}
|
125
125
|
}
|
126
126
|
|
127
127
|
.g-root .data-table_highlight-rows .data-table__row:hover {
|