trust-module-frontend/src/Charts2/PrometheusChart.jsx

313 lines
12 KiB
JavaScript

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 (
<div>
<div style={{ marginBottom: 16 }}>
<Radio.Group
value={mode}
onChange={(e) => setMode(e.target.value)}
buttonStyle="solid"
style={{ marginBottom: 10 }}
>
<Radio.Button value="realtime">Режим реального времени</Radio.Button>
<Radio.Button value="historical">Исторические данные</Radio.Button>
</Radio.Group>
{mode === 'historical' && (
<DateRangeSelector
startDate={startDate}
endDate={endDate}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
onApply={handleCustomRangeApply}
/>
)}
{mode === 'realtime' && isLiveUpdating && (
<Button
type="primary"
danger
onClick={() => setMode('historical')}
style={{ marginTop: 10 }}
>
Остановить обновление
</Button>
)}
</div>
{device && <Tag color="geekblue">Устройство: {device}</Tag>}
{source_id && <Tag color="purple">Модуль: {source_id.split('$')[1]}</Tag>}
<Box position="relative">
<MuiTooltip title={showLogs ? "Скрыть логи" : "Показать логи"}>
<IconButton
onClick={() => setShowLogs(!showLogs)}
sx={{
position: 'absolute',
right: 16,
top: 16,
zIndex: 1000,
bgcolor: 'background.paper',
boxShadow: 1
}}
>
<ListAlt />
</IconButton>
</MuiTooltip>
{isLoading ? (
<div>Загрузка графика...</div>
) : error ? (
<div>Ошибка: {error}</div>
) : chartData.length === 0 ? (
<div>Нет данных для метрики: {metricName}</div>
) : (
<>
<LineChartComponent
data={chartData}
title={title}
description={description}
metaInfo={metaInfo}
height={chartHeight}
additionalFilters={{
device,
source_id
}}
ranges={ranges}
/>
{showLogs && (
<StatusLogTable logs={statusLogs} />
)}
</>
)}
</Box>
</div>
);
};
export default PrometheusChart;