ydb-embedded-ui 4.29.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.
- 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 {
|