import React, { useState, useEffect } from 'react'; import LineChartComponent from './Components/LineChartComponent'; import DateRangeSelector from './Components/DateRangeSelector'; import metricsService from './Components/metricsService'; import { Button, Radio, message, Tag } from 'antd'; import moment from 'moment'; import StatusLogTable from './Components/StatusLogTable'; import { Box, IconButton, Tooltip as MuiTooltip } from '@mui/material'; import { ListAlt } from '@mui/icons-material'; const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => { const { name: metricName, filters = {}, title = metricName, description, context = {}, ranges = [] } = metricInfo || {}; const { device, source_id } = context; const [chartData, setChartData] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [metricMeta, setMetricMeta] = useState({}); const [mode, setMode] = useState('realtime'); const [startDate, setStartDate] = useState(moment().subtract(1, 'hour').toDate()); const [endDate, setEndDate] = useState(moment().toDate()); const [isLiveUpdating, setIsLiveUpdating] = useState(false); const [showLogs, setShowLogs] = useState(false); const [statusLogs, setStatusLogs] = useState([]); const MAX_POINTS = 50; const TIME_WINDOW_MS = 3600 * 1000; // 1 час в миллисекундах const getSubscriptionKey = () => { const filterParts = []; if (device) filterParts.push(`device=${encodeURIComponent(device)}`); if (source_id) filterParts.push(`source_id=${encodeURIComponent(source_id)}`); return `${metricName}${filterParts.length ? `?${filterParts.join('&')}` : ''}`; }; const formatMetricData = (dataArray) => { return dataArray .map(item => ({ ...item, timestamp: item.timestamp, value: parseFloat(item.value), name: item.__name__ || metricName, status: parseInt(item.status) || 0, device: item.device?.trim() || null, source_id: item.source_id || null, description: item.description || description })) .sort((a, b) => a.timestamp - b.timestamp); }; const calculateStep = (startTime, endTime, maxPoints = 10000) => { const durationSeconds = (endTime.getTime() - startTime.getTime()) / 1000; return Math.max(Math.ceil(durationSeconds / maxPoints), 1); }; const downsampleData = (data, maxPoints = MAX_POINTS) => { if (data.length <= maxPoints) return [...data]; const sortedData = [...data].sort((a, b) => a.timestamp - b.timestamp); const step = Math.max(1, Math.floor(sortedData.length / maxPoints)); const result = []; for (let i = 0; i < sortedData.length; i += step) { if (result.length >= maxPoints) break; result.push(sortedData[i]); } // Всегда включаем последнюю точку if (result.length > 0) { const lastOriginalPoint = sortedData[sortedData.length - 1]; if (result[result.length - 1].timestamp !== lastOriginalPoint.timestamp) { result[result.length - 1] = lastOriginalPoint; } } return result; }; // Обновляем логи при изменении данных useEffect(() => { if (chartData.length > 0) { const newLogs = chartData.reduce((acc, point, index) => { if (index === 0 || point.status !== chartData[index - 1].status) { return [...acc, point]; } return acc; }, []); setStatusLogs(newLogs); } }, [chartData]); const fetchHistoricalData = async (start, end) => { setIsLoading(true); setError(null); try { const extendedFilters = { ...filters, ...(device && { device: device.toString() }), ...(source_id && { source_id: source_id.toString() }) }; const step = calculateStep(start, end); const data = await metricsService.fetchMetricsRange( metricName, Math.floor(start.getTime() / 1000), Math.floor(end.getTime() / 1000), step, extendedFilters ); const formattedData = formatMetricData(data) .sort((a, b) => a.timestamp - b.timestamp); // Применяем ограничение по количеству точек только для исторических данных const limitedData = formattedData.length > MAX_POINTS ? formattedData.slice(-MAX_POINTS) : formattedData; if (limitedData.length > 0) { setMetricMeta({ type: data[0]?.type, description: data[0]?.description || description, instance: data[0]?.instance, job: data[0]?.job }); } setChartData(limitedData); } catch (err) { console.error(`Error loading historical data for ${metricName}:`, err); setError(err.message); message.error(`Failed to load historical data: ${err.message}`); } finally { setIsLoading(false); } }; const startRealtimeUpdates = () => { setIsLiveUpdating(true); setIsLoading(true); const end = new Date(); const start = new Date(end.getTime() - TIME_WINDOW_MS); fetchHistoricalData(start, end).finally(() => setIsLoading(false)); return metricsService.subscribeToMetric( getSubscriptionKey(), (newData) => { setChartData(prev => { const now = Date.now(); const cutoffTime = now - TIME_WINDOW_MS; // Фильтруем старые точки (старше TIME_WINDOW_MS) const filteredPrev = prev.filter(point => point.timestamp >= cutoffTime); // Добавляем новые точки const newPoints = formatMetricData(newData) .filter(point => point.timestamp >= cutoffTime); // Объединяем и удаляем дубликаты const mergedData = [...filteredPrev, ...newPoints] .filter((v, i, a) => a.findIndex(t => t.timestamp === v.timestamp) === i) .sort((a, b) => a.timestamp - b.timestamp); // Если точек слишком много, равномерно прореживаем if (mergedData.length > MAX_POINTS) { const step = Math.ceil(mergedData.length / MAX_POINTS); return mergedData.filter((_, index) => index % step === 0); } return mergedData; }); }, 1000, // Уменьшаем интервал обновления до 1 секунды { ...filters, ...(device && { device }), ...(source_id && { source_id }) } ); }; const stopRealtimeUpdates = () => { setIsLiveUpdating(false); metricsService.unsubscribeFromMetric(getSubscriptionKey()); }; const handleCustomRangeApply = () => { if (startDate && endDate) { fetchHistoricalData(startDate, endDate); } }; useEffect(() => { console.log('Current metric context:', { device, source_id, metricName }); let unsubscribe; if (mode === 'realtime') { unsubscribe = startRealtimeUpdates(); } else { stopRealtimeUpdates(); fetchHistoricalData(startDate, endDate); } return () => { if (unsubscribe) unsubscribe(); stopRealtimeUpdates(); }; }, [mode, metricName, device, source_id]); const metaInfo = [ metricMeta.instance && `Instance: ${metricMeta.instance}`, metricMeta.job && `Job: ${metricMeta.job}`, metricMeta.type && `Type: ${metricMeta.type}` ].filter(Boolean).join(' | '); return (