removed unnecessary components

authorization-token
DmitriyA 2025-04-21 03:28:14 -04:00
parent c7ebbcaf5c
commit f38c8825fe
3 changed files with 2 additions and 446 deletions

View File

@ -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;
}

View File

@ -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);

View File

@ -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,