removed unnecessary components
parent
c7ebbcaf5c
commit
f38c8825fe
|
|
@ -1,153 +0,0 @@
|
||||||
// src/utils/metricsUtils.js
|
|
||||||
import { MINUTE, HOUR, DAY } from './constants';
|
|
||||||
|
|
||||||
export function formatTime(timestamp, rangeSeconds) {
|
|
||||||
const ts = typeof timestamp === 'number' ? timestamp : Date.now();
|
|
||||||
const date = new Date(ts);
|
|
||||||
|
|
||||||
const timeOptions = {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit',
|
|
||||||
hour12: false
|
|
||||||
};
|
|
||||||
|
|
||||||
const dateOptions = rangeSeconds > 86400 ? {
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
...timeOptions
|
|
||||||
} : timeOptions;
|
|
||||||
|
|
||||||
return {
|
|
||||||
display: date.toLocaleString('ru-RU', dateOptions),
|
|
||||||
fullDisplay: date.toLocaleString('ru-RU', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
...timeOptions
|
|
||||||
}),
|
|
||||||
timestamp: ts
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function calculateStep(start, end) {
|
|
||||||
const rangeSeconds = end - start;
|
|
||||||
|
|
||||||
if (rangeSeconds <= MINUTE) return 1;
|
|
||||||
if (rangeSeconds <= 5 * MINUTE) return 5;
|
|
||||||
if (rangeSeconds <= 15 * MINUTE) return 15;
|
|
||||||
if (rangeSeconds <= HOUR) return 30;
|
|
||||||
if (rangeSeconds <= 3 * HOUR) return 2 * MINUTE;
|
|
||||||
if (rangeSeconds <= 6 * HOUR) return 5 * MINUTE;
|
|
||||||
if (rangeSeconds <= 12 * HOUR) return 10 * MINUTE;
|
|
||||||
if (rangeSeconds <= DAY) return 15 * MINUTE;
|
|
||||||
if (rangeSeconds <= 3 * DAY) return HOUR;
|
|
||||||
return 2 * HOUR;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function processMetricsData(metricName, responseData, prevData, rangeSeconds) {
|
|
||||||
if (!responseData) {
|
|
||||||
console.error('No data received for processing');
|
|
||||||
return prevData || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавим обработку случая, когда данные приходят в формате {metric, data, metadata}
|
|
||||||
const rawData = responseData.data || (Array.isArray(responseData) ? responseData : [responseData]);
|
|
||||||
|
|
||||||
const newData = { ...(prevData || {}) };
|
|
||||||
|
|
||||||
rawData.forEach(item => {
|
|
||||||
try {
|
|
||||||
const instance = item.instance || item.metric?.instance || 'default';
|
|
||||||
if (!newData[instance]) newData[instance] = [];
|
|
||||||
|
|
||||||
// Обработка timestamp
|
|
||||||
let timestamp = item.timestamp;
|
|
||||||
if (typeof timestamp !== 'number') {
|
|
||||||
timestamp = Date.now();
|
|
||||||
} else if (timestamp < 1e12) { // Если timestamp в секундах
|
|
||||||
timestamp *= 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработка value
|
|
||||||
let value = item.value;
|
|
||||||
if (value === undefined && item.metric?.value !== undefined) {
|
|
||||||
value = item.metric.value;
|
|
||||||
}
|
|
||||||
if (typeof value !== 'number') {
|
|
||||||
value = parseFloat(value);
|
|
||||||
if (isNaN(value)) {
|
|
||||||
console.warn('Invalid value, using 0 as fallback:', item);
|
|
||||||
value = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedTime = formatTime(timestamp, rangeSeconds);
|
|
||||||
|
|
||||||
newData[instance].push({
|
|
||||||
time: formattedTime.display,
|
|
||||||
fullTime: formattedTime.fullDisplay,
|
|
||||||
value: value,
|
|
||||||
timestamp: timestamp,
|
|
||||||
meta: {
|
|
||||||
description: item.description || item.metric?.description,
|
|
||||||
type: item.type || item.metric?.type,
|
|
||||||
status: item.status || item.metric?.status
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing metric item:', item, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Сортировка и ограничение данных
|
|
||||||
Object.keys(newData).forEach(instance => {
|
|
||||||
newData[instance] = newData[instance]
|
|
||||||
.sort((a, b) => a.timestamp - b.timestamp)
|
|
||||||
.slice(-1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
return newData;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function interpolateData(data, targetPointCount, timeRangeSeconds) {
|
|
||||||
if (!data || data.length < 2) return data || [];
|
|
||||||
if (data.length >= targetPointCount) return data;
|
|
||||||
|
|
||||||
const interpolated = [];
|
|
||||||
const step = (data.length - 1) / (targetPointCount - 1);
|
|
||||||
|
|
||||||
for (let i = 0; i < targetPointCount; i++) {
|
|
||||||
const index = i * step;
|
|
||||||
const lowerIndex = Math.floor(index);
|
|
||||||
const upperIndex = Math.ceil(index);
|
|
||||||
|
|
||||||
if (lowerIndex === upperIndex) {
|
|
||||||
interpolated.push(data[lowerIndex]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fraction = index - lowerIndex;
|
|
||||||
const lower = data[lowerIndex];
|
|
||||||
const upper = data[upperIndex];
|
|
||||||
|
|
||||||
const interpolatedPoint = {
|
|
||||||
time: '',
|
|
||||||
fullTime: '',
|
|
||||||
value: lower.value + fraction * (upper.value - lower.value),
|
|
||||||
timestamp: lower.timestamp + fraction * (upper.timestamp - lower.timestamp)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Форматирование времени
|
|
||||||
const formatted = formatTime(interpolatedPoint.timestamp, timeRangeSeconds || DAY);
|
|
||||||
interpolatedPoint.time = formatted.display;
|
|
||||||
interpolatedPoint.fullTime = formatted.fullDisplay;
|
|
||||||
|
|
||||||
interpolated.push(interpolatedPoint);
|
|
||||||
|
|
||||||
console.log('Item:', item.value, timestamp, formattedTime.display);
|
|
||||||
}
|
|
||||||
|
|
||||||
return interpolated;
|
|
||||||
}
|
|
||||||
|
|
@ -1,291 +0,0 @@
|
||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
||||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceArea } from 'recharts';
|
|
||||||
import io from 'socket.io-client';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { Select, Button, Space, DatePicker, Spin, Alert } from 'antd';
|
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
const { Option } = Select;
|
|
||||||
const { RangePicker } = DatePicker;
|
|
||||||
|
|
||||||
const timeRanges = [
|
|
||||||
{ label: '1 мин', value: 1 },
|
|
||||||
{ label: '5 мин', value: 5 },
|
|
||||||
{ label: '30 мин', value: 30 },
|
|
||||||
{ label: '1 час', value: 60 },
|
|
||||||
{ label: '3 часа', value: 180 },
|
|
||||||
{ label: '6 часов', value: 360 },
|
|
||||||
{ label: '12 часов', value: 720 },
|
|
||||||
{ label: '24 часа', value: 1440 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const getStatusColor = (status) => {
|
|
||||||
if (!status) return '#1890ff';
|
|
||||||
switch (status.toUpperCase()) {
|
|
||||||
case 'OK': return '#52c41a';
|
|
||||||
case 'WARNING': return '#faad14';
|
|
||||||
case 'CRITICAL': return '#f5222d';
|
|
||||||
default: return '#1890ff';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const MetricChart = ({ metricName, title }) => {
|
|
||||||
const [data, setData] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
const [selectedRange, setSelectedRange] = useState(timeRanges[0]);
|
|
||||||
const [customRange, setCustomRange] = useState([]); // Используем массив вместо null для RangePicker
|
|
||||||
const [isLive, setIsLive] = useState(true);
|
|
||||||
const [refAreaLeft, setRefAreaLeft] = useState(null);
|
|
||||||
const [refAreaRight, setRefAreaRight] = useState(null);
|
|
||||||
const socketRef = useRef(null);
|
|
||||||
const dataRef = useRef([]);
|
|
||||||
|
|
||||||
// Форматирование данных для графика
|
|
||||||
const formatData = useCallback((rawData) => {
|
|
||||||
if (!Array.isArray(rawData)) {
|
|
||||||
console.error('Expected array but received:', rawData);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return rawData.map(item => ({
|
|
||||||
timestamp: item.timestamp,
|
|
||||||
time: moment(item.timestamp).format('HH:mm:ss'),
|
|
||||||
value: parseFloat(item.value) || 0,
|
|
||||||
status: item.status
|
|
||||||
}));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Загрузка исторических данных
|
|
||||||
const fetchHistoricalData = useCallback(async (start, end) => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const duration = moment.duration(end.diff(start)).asMinutes();
|
|
||||||
const step = Math.max(1, Math.floor(duration / 100)) + 's';
|
|
||||||
|
|
||||||
const response = await axios.get(`${import.meta.env.VITE_BACK_HTTP_URL}/metrics`, {
|
|
||||||
params: {
|
|
||||||
metric: metricName,
|
|
||||||
start: start.valueOf(),
|
|
||||||
end: end.valueOf(),
|
|
||||||
step: step
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json' // Убедимся, что получаем JSON
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.headers['content-type'].includes('text/html')) {
|
|
||||||
throw new Error('Server returned HTML instead of JSON. Check your API endpoint.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedData = formatData(response.data);
|
|
||||||
dataRef.current = formattedData;
|
|
||||||
setData(formattedData);
|
|
||||||
setIsLive(false);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.response?.data?.message || err.message || 'Failed to fetch data');
|
|
||||||
console.error('Error fetching historical data:', err);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [metricName, formatData]);
|
|
||||||
|
|
||||||
// Подключение к WebSocket и загрузка начальных данных
|
|
||||||
const connectWebSocket = useCallback(() => {
|
|
||||||
if (socketRef.current) {
|
|
||||||
socketRef.current.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
socketRef.current = io(`${import.meta.env.VITE_BACK_WS_URL}/api/metrics-ws`, {
|
|
||||||
transports: ['websocket'],
|
|
||||||
reconnectionAttempts: 5
|
|
||||||
});
|
|
||||||
|
|
||||||
socketRef.current.on('connect', () => {
|
|
||||||
console.log('WebSocket connected');
|
|
||||||
socketRef.current.emit('subscribe-metric', {
|
|
||||||
metric: metricName,
|
|
||||||
interval: 5000
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
socketRef.current.on('metrics-data', (response) => {
|
|
||||||
if (response.metric === metricName && response.data) {
|
|
||||||
try {
|
|
||||||
const newDataPoint = formatData([response.data])[0]; // Оборачиваем в массив
|
|
||||||
if (newDataPoint) {
|
|
||||||
dataRef.current = [...dataRef.current, newDataPoint].slice(-1000);
|
|
||||||
if (isLive) {
|
|
||||||
const now = moment();
|
|
||||||
const cutoff = now.subtract(selectedRange.value, 'minutes');
|
|
||||||
setData(dataRef.current.filter(item => moment(item.timestamp).isAfter(cutoff)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error processing WebSocket data:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socketRef.current.on('error', (err) => {
|
|
||||||
setError(err.message || 'WebSocket error');
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (socketRef.current) {
|
|
||||||
socketRef.current.emit('unsubscribe-metric');
|
|
||||||
socketRef.current.disconnect();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [metricName, formatData, isLive, selectedRange.value]);
|
|
||||||
|
|
||||||
// Обработчики изменения диапазона
|
|
||||||
const handleRangeChange = (value) => {
|
|
||||||
const range = timeRanges.find(r => r.value === value);
|
|
||||||
if (!range) return;
|
|
||||||
|
|
||||||
setSelectedRange(range);
|
|
||||||
setCustomRange([]); // Сбрасываем кастомный диапазон
|
|
||||||
setIsLive(true);
|
|
||||||
|
|
||||||
const now = moment();
|
|
||||||
const cutoff = now.subtract(range.value, 'minutes');
|
|
||||||
setData(dataRef.current.filter(item => moment(item.timestamp).isAfter(cutoff)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCustomRange = (dates) => {
|
|
||||||
if (!dates || dates.length !== 2) {
|
|
||||||
setCustomRange([]);
|
|
||||||
setIsLive(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [start, end] = dates;
|
|
||||||
setCustomRange(dates);
|
|
||||||
fetchHistoricalData(start, end);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Эффекты
|
|
||||||
useEffect(() => {
|
|
||||||
if (isLive) {
|
|
||||||
const cleanup = connectWebSocket();
|
|
||||||
// Загружаем начальные данные
|
|
||||||
const end = moment();
|
|
||||||
const start = end.clone().subtract(selectedRange.value, 'minutes');
|
|
||||||
fetchHistoricalData(start, end);
|
|
||||||
return cleanup;
|
|
||||||
}
|
|
||||||
}, [isLive, connectWebSocket, selectedRange.value, fetchHistoricalData]);
|
|
||||||
|
|
||||||
// Обработчики для zoom на графике
|
|
||||||
const handleMouseDown = (e) => {
|
|
||||||
if (!e || !e.activeLabel) return;
|
|
||||||
setRefAreaLeft(e.activeLabel);
|
|
||||||
setRefAreaRight(e.activeLabel);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseMove = (e) => {
|
|
||||||
if (!refAreaLeft || !e.activeLabel) return;
|
|
||||||
setRefAreaRight(e.activeLabel);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
|
||||||
if (!refAreaLeft || !refAreaRight) return;
|
|
||||||
|
|
||||||
const leftIdx = data.findIndex(d => d.time === refAreaLeft);
|
|
||||||
const rightIdx = data.findIndex(d => d.time === refAreaRight);
|
|
||||||
|
|
||||||
if (leftIdx !== -1 && rightIdx !== -1) {
|
|
||||||
const start = moment(Math.min(data[leftIdx].timestamp, data[rightIdx].timestamp));
|
|
||||||
const end = moment(Math.max(data[leftIdx].timestamp, data[rightIdx].timestamp));
|
|
||||||
fetchHistoricalData(start, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
setRefAreaLeft(null);
|
|
||||||
setRefAreaRight(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBackToLive = () => {
|
|
||||||
setIsLive(true);
|
|
||||||
setCustomRange([]);
|
|
||||||
setSelectedRange(timeRanges[0]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ width: '100%', height: 400 }}>
|
|
||||||
<h3>{title}</h3>
|
|
||||||
|
|
||||||
<Space style={{ marginBottom: 16 }}>
|
|
||||||
<Select
|
|
||||||
value={selectedRange.value}
|
|
||||||
onChange={handleRangeChange}
|
|
||||||
disabled={!isLive}
|
|
||||||
style={{ width: 120 }}
|
|
||||||
>
|
|
||||||
{timeRanges.map(range => (
|
|
||||||
<Option key={range.value} value={range.value}>{range.label}</Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<RangePicker
|
|
||||||
showTime={{ format: 'HH:mm' }}
|
|
||||||
format="YYYY-MM-DD HH:mm"
|
|
||||||
onChange={handleCustomRange}
|
|
||||||
disabled={!isLive}
|
|
||||||
value={customRange} // Устанавливаем значение для избежания предупреждения
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!isLive && (
|
|
||||||
<Button onClick={handleBackToLive}>Реальное время</Button>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
|
|
||||||
{error && <Alert message={error} type="error" showIcon />}
|
|
||||||
{loading && <Spin tip="Loading..." size="large" />}
|
|
||||||
|
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
|
||||||
<AreaChart
|
|
||||||
data={data}
|
|
||||||
onMouseDown={handleMouseDown}
|
|
||||||
onMouseMove={handleMouseMove}
|
|
||||||
onMouseUp={handleMouseUp}
|
|
||||||
>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="time" />
|
|
||||||
<YAxis />
|
|
||||||
<Tooltip
|
|
||||||
formatter={(value, name, props) => [
|
|
||||||
`${value} (${props.payload?.status || 'N/A'})`,
|
|
||||||
name
|
|
||||||
]}
|
|
||||||
labelFormatter={(label) => {
|
|
||||||
// Исправляем предупреждение Moment.js
|
|
||||||
if (!label) return '';
|
|
||||||
return moment(label, 'HH:mm:ss').isValid()
|
|
||||||
? moment(label, 'HH:mm:ss').format('YYYY-MM-DD HH:mm:ss')
|
|
||||||
: label;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
type="monotone"
|
|
||||||
dataKey="value"
|
|
||||||
stroke="#1890ff"
|
|
||||||
fill="#1890ff"
|
|
||||||
isAnimationActive={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{refAreaLeft && refAreaRight && (
|
|
||||||
<ReferenceArea
|
|
||||||
x1={refAreaLeft}
|
|
||||||
x2={refAreaRight}
|
|
||||||
strokeOpacity={0.3}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default React.memo(MetricChart);
|
|
||||||
|
|
@ -48,9 +48,9 @@ const MainContent = styled(Box)(({ theme }) => ({
|
||||||
|
|
||||||
const Content = styled(Box)(({ theme }) => ({
|
const Content = styled(Box)(({ theme }) => ({
|
||||||
backgroundColor: theme.palette.custom.modalBackground,
|
backgroundColor: theme.palette.custom.modalBackground,
|
||||||
padding: theme.spacing(2.5),
|
//padding: theme.spacing(2.5),
|
||||||
borderRadius: '10px',
|
borderRadius: '10px',
|
||||||
boxShadow: theme.shadows[2],
|
//boxShadow: theme.shadows[2],
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
color: theme.palette.custom.modalText,
|
color: theme.palette.custom.modalText,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue