313 lines
12 KiB
JavaScript
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; |