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 }) => ({
|
||||
backgroundColor: theme.palette.custom.modalBackground,
|
||||
padding: theme.spacing(2.5),
|
||||
//padding: theme.spacing(2.5),
|
||||
borderRadius: '10px',
|
||||
boxShadow: theme.shadows[2],
|
||||
//boxShadow: theme.shadows[2],
|
||||
maxWidth: '100%',
|
||||
overflow: 'auto',
|
||||
color: theme.palette.custom.modalText,
|
||||
|
|
|
|||
Loading…
Reference in New Issue