added complex variables

pull/59/head
SovietSpiderCat 2025-08-22 09:57:16 +03:00
parent 421d95565c
commit 205ddc71e0
3 changed files with 201 additions and 144 deletions

View File

@ -16,13 +16,13 @@ const formatXAxis = (tickItem) => {
}; };
const formatTooltip = (value, name, props) => { const formatTooltip = (value, name, props) => {
return [`${value.toFixed(2)}`, ` ${name}`]; return [`${value.toFixed(2)}`, `Устройство ${name}`];
}; };
const LineChartComponent = ({ const LineChartComponent = ({
data = [], data = [],
multipleLines = false, multipleLines = true, // По умолчанию включаем множественные линии
lineKey = 'device', lineKey = 'device', // Ключ для разделения линий
title, title,
description, description,
height = 400, height = 400,
@ -30,27 +30,25 @@ const LineChartComponent = ({
}) => { }) => {
if (!data || data.length === 0) return <div>Нет данных для отображения</div>; if (!data || data.length === 0) return <div>Нет данных для отображения</div>;
// Создаем массив уникальных линий // Создаем массив уникальных устройств
const lineKeys = [...new Set(data.map(item => item[lineKey] || 'default'))]; const devices = [...new Set(data.map(item => item.device))];
// Преобразуем данные в формат, удобный для Recharts // Группируем данные по timestamp для правильного отображения
const chartData = data.reduce((acc, item) => { const timestamps = [...new Set(data.map(item => item.timestamp))].sort();
const timestamp = item.timestamp;
const existingPoint = acc.find(p => p.timestamp === timestamp);
if (existingPoint) { const chartData = timestamps.map(timestamp => {
return acc.map(p => const point = { timestamp };
p.timestamp === timestamp
? { ...p, [item[lineKey] || 'default']: item.value } // Для каждого устройства находим значение в этот timestamp
: p devices.forEach(device => {
const deviceData = data.find(item =>
item.timestamp === timestamp && item.device === device
); );
} point[`device_${device}`] = deviceData ? deviceData.value : null;
});
return [...acc, { return point;
timestamp, });
[item[lineKey] || 'default']: item.value
}];
}, []).sort((a, b) => a.timestamp - b.timestamp);
return ( return (
<div style={{ width: '100%', height: `${height}px` }}> <div style={{ width: '100%', height: `${height}px` }}>
@ -63,39 +61,27 @@ const LineChartComponent = ({
dataKey="timestamp" dataKey="timestamp"
tickFormatter={formatXAxis} tickFormatter={formatXAxis}
/> />
<YAxis domain={[0, 25]} /> <YAxis />
<Tooltip <Tooltip
formatter={formatTooltip} formatter={formatTooltip}
labelFormatter={(label) => format(new Date(label), 'yyyy-MM-dd HH:mm:ss')} labelFormatter={(label) => format(new Date(label), 'yyyy-MM-dd HH:mm:ss')}
/> />
<Legend /> <Legend />
{multipleLines ? ( {devices.map(device => (
lineKeys.map(key => (
<Line <Line
key={`line-${key}`} key={`line-${device}`}
type="monotone" type="monotone"
dataKey={key} dataKey={`device_${device}`}
name={` ${key}`} name={`Устройство ${device}`}
stroke={lineColors[key] || lineColors.default} stroke={lineColors[device] || lineColors.default}
strokeWidth={2} strokeWidth={2}
dot={false} dot={false}
activeDot={{ r: 6 }} activeDot={{ r: 6 }}
isAnimationActive={false} isAnimationActive={false}
connectNulls={true}
/> />
)) ))}
) : (
<Line
type="monotone"
dataKey={lineKeys[0] || 'value'}
name={title}
stroke={lineColors.default}
strokeWidth={2}
dot={false}
activeDot={{ r: 6 }}
isAnimationActive={false}
/>
)}
{/* Добавляем диапазоны если они есть */} {/* Добавляем диапазоны если они есть */}
{ranges.map((range, idx) => ( {ranges.map((range, idx) => (

View File

@ -1,8 +1,8 @@
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect } from 'react';
import LineChartComponent from './LineChartComponent'; import LineChartComponent from './LineChartComponent';
import DateRangeSelector from '../Charts2/Components/DateRangeSelector'; import DateRangeSelector from '../Charts2/Components/DateRangeSelector';
import metricsService from '../Charts2/Components/metricsService'; import metricsService from '../Charts2/Components/metricsService';
import { Button, Radio, message, Tag, Spin } from 'antd'; import { Button, Radio, message, Tag } from 'antd';
import moment from 'moment'; import moment from 'moment';
import StatusLogTable from '../Charts2/Components/StatusLogTable'; import StatusLogTable from '../Charts2/Components/StatusLogTable';
import { Box, IconButton, Tooltip as MuiTooltip } from '@mui/material'; import { Box, IconButton, Tooltip as MuiTooltip } from '@mui/material';
@ -15,14 +15,12 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => {
title = metricName, title = metricName,
description, description,
context = {}, context = {},
ranges = [], ranges = []
multipleLines = false,
lineKey = 'device'
} = metricInfo || {}; } = metricInfo || {};
const { device, source_id } = context; const { device, source_id } = context;
const [rawData, setRawData] = useState([]); const [chartData, setChartData] = useState([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [metricMeta, setMetricMeta] = useState({}); const [metricMeta, setMetricMeta] = useState({});
@ -32,43 +30,96 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => {
const [isLiveUpdating, setIsLiveUpdating] = useState(false); const [isLiveUpdating, setIsLiveUpdating] = useState(false);
const [showLogs, setShowLogs] = useState(false); const [showLogs, setShowLogs] = useState(false);
const [statusLogs, setStatusLogs] = useState([]); const [statusLogs, setStatusLogs] = useState([]);
const MAX_POINTS = 50;
const MAX_POINTS = 1000;
const TIME_WINDOW_MS = 3600 * 1000; const TIME_WINDOW_MS = 3600 * 1000;
const subscriptionKey = useMemo(() => {
// Эта функция может больше не понадобиться, так как
// сервис сам генерирует ключи, но оставьте для совместимости
const getSubscriptionKey = () => {
const filterParts = []; const filterParts = [];
if (device) filterParts.push(`device=${encodeURIComponent(device)}`); if (device) filterParts.push(`device=${encodeURIComponent(device)}`);
if (source_id) filterParts.push(`source_id=${encodeURIComponent(source_id)}`); if (source_id) filterParts.push(`source_id=${encodeURIComponent(source_id)}`);
return `${metricName}${filterParts.length ? `?${filterParts.join('&')}` : ''}`; return `${metricName}${filterParts.length ? `?${filterParts.join('&')}` : ''}`;
}, [metricName, device, source_id]); };
const formatMetricData = (responseData) => { const getStatusFromRanges = (value, ranges) => {
const dataArray = Array.isArray(responseData) ? responseData : responseData.data; if (!ranges || ranges.length === 0) return 1;
for (const r of ranges) {
if (value >= r.min && value <= r.max) {
return r.status;
}
}
return 1;
};
const formatMetricData = (dataArray) => {
if (!Array.isArray(dataArray)) { if (!Array.isArray(dataArray)) {
console.error('Expected array but got:', responseData); console.error('Expected array in formatMetricData, got:', typeof dataArray);
return []; return [];
} }
return dataArray.map(item => ({ return dataArray.map(item => {
if (item.timestamp === undefined || item.value === undefined) {
console.warn('Invalid metric item:', item);
return null;
}
return {
...item, ...item,
timestamp: item.timestamp, timestamp: Number(item.timestamp),
value: parseFloat(item.value), value: parseFloat(item.value),
status: getStatusFromRanges(parseFloat(item.value), ranges),
name: item.__name__ || metricName, name: item.__name__ || metricName,
status: parseInt(item.status) || 0,
device: item.device?.trim() || null, device: item.device?.trim() || null,
source_id: item.source_id || null, source_id: item.source_id || null,
description: item.description || description, description: item.description || description
lineId: item[lineKey] || 'default' };
})); }).filter(Boolean)
.sort((a, b) => a.timestamp - b.timestamp);
}; };
const calculateStep = (start, end) => { const calculateStep = (startTime, endTime, maxPoints = 10000) => {
const duration = end.getTime() - start.getTime(); const durationSeconds = (endTime.getTime() - startTime.getTime()) / 1000;
return Math.max(Math.floor(duration / (MAX_POINTS * 1000)), 1); 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) => { const fetchHistoricalData = async (start, end) => {
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
@ -82,19 +133,23 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => {
const step = calculateStep(start, end); const step = calculateStep(start, end);
// Используем новый метод для исторических данных
const data = await metricsService.fetchMetricsRange( const data = await metricsService.fetchMetricsRange(
metricName, metricName,
Math.floor(start.getTime() / 1000), start.getTime(), // Теперь передаем timestamp в миллисекундах
Math.floor(end.getTime() / 1000), end.getTime(),
step, step,
extendedFilters extendedFilters
); );
const responseData = Array.isArray(data) ? data : data.data; const formattedData = formatMetricData(data)
const formattedData = formatMetricData(responseData); .sort((a, b) => a.timestamp - b.timestamp);
setRawData(formattedData);
if (formattedData.length > 0) { const limitedData = formattedData.length > MAX_POINTS
? downsampleData(formattedData, MAX_POINTS)
: formattedData;
if (limitedData.length > 0) {
setMetricMeta({ setMetricMeta({
type: data[0]?.type, type: data[0]?.type,
description: data[0]?.description || description, description: data[0]?.description || description,
@ -102,6 +157,8 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => {
job: data[0]?.job job: data[0]?.job
}); });
} }
setChartData(limitedData);
} catch (err) { } catch (err) {
console.error(`Error loading historical data for ${metricName}:`, err); console.error(`Error loading historical data for ${metricName}:`, err);
setError(err.message); setError(err.message);
@ -117,42 +174,55 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => {
const end = new Date(); const end = new Date();
const start = new Date(end.getTime() - TIME_WINDOW_MS); const start = new Date(end.getTime() - TIME_WINDOW_MS);
const cutoffTime = Date.now() - TIME_WINDOW_MS;
fetchHistoricalData(start, end).finally(() => setIsLoading(false)); fetchHistoricalData(start, end).finally(() => setIsLoading(false));
// Изменяем параметры подписки
return metricsService.subscribeToMetric( return metricsService.subscribeToMetric(
subscriptionKey, metricName, // Теперь передаем просто имя метрики
(newData) => { { ...filters, device, source_id }, // Фильры отдельным параметром
setRawData(prev => { (update) => { // Колбэк получает объект с данными
const actualData = Array.isArray(newData) ? newData : newData.data; console.log('Received WS update:', update);
const formattedNewData = formatMetricData(actualData)
if (!update || !Array.isArray(update.data)) {
console.error('Invalid update format:', update);
return;
}
setChartData(prev => {
const now = Date.now();
const cutoffTime = now - TIME_WINDOW_MS;
const formattedNew = formatMetricData(update.data)
.filter(point => point.timestamp >= cutoffTime); .filter(point => point.timestamp >= cutoffTime);
const filteredPrev = prev.filter(point => point.timestamp >= cutoffTime); const filteredPrev = prev.filter(point =>
point.timestamp >= cutoffTime
const merged = [...filteredPrev, ...formattedNewData]
.filter((v, i, a) =>
a.findIndex(t =>
t.timestamp === v.timestamp &&
t[lineKey] === v[lineKey]
) === i
); );
return merged; const merged = [...filteredPrev, ...formattedNew]
.filter((v, i, a) =>
a.findIndex(t => t.timestamp === v.timestamp) === i
)
.sort((a, b) => a.timestamp - b.timestamp);
return merged.length > MAX_POINTS
? merged.slice(-MAX_POINTS)
: merged;
}); });
}, },
5000, 5000 // Интервал обновления (можно настроить)
{
...filters,
...(device && { device }),
...(source_id && { source_id })
}
); );
}; };
const stopRealtimeUpdates = () => { const stopRealtimeUpdates = () => {
setIsLiveUpdating(false); setIsLiveUpdating(false);
metricsService.unsubscribeFromMetric(subscriptionKey); // Теперь отписываемся по метрике и фильтрам
metricsService.unsubscribeFromMetric(
metricName,
{ ...filters, device, source_id }
);
}; };
const handleCustomRangeApply = () => { const handleCustomRangeApply = () => {
@ -162,45 +232,29 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => {
}; };
useEffect(() => { useEffect(() => {
if (rawData.length > 0) { console.log('Metric changed:', { metricName, device, source_id, filters });
const logs = [];
const devices = [...new Set(rawData.map(item => item[lineKey]))];
devices.forEach(dev => {
const deviceData = rawData
.filter(item => item[lineKey] === dev)
.sort((a, b) => a.timestamp - b.timestamp);
if (deviceData.length > 0) {
logs.push(deviceData[0]); // Первая точка
for (let i = 1; i < deviceData.length; i++) {
if (deviceData[i].status !== deviceData[i - 1].status) {
logs.push(deviceData[i]);
}
}
}
});
setStatusLogs(logs.sort((a, b) => b.timestamp - a.timestamp));
}
}, [rawData, lineKey]);
useEffect(() => {
let unsubscribe; let unsubscribe;
const init = async () => {
if (mode === 'realtime') { if (mode === 'realtime') {
unsubscribe = startRealtimeUpdates(); unsubscribe = startRealtimeUpdates();
} else { } else {
stopRealtimeUpdates(); await fetchHistoricalData(startDate, endDate);
fetchHistoricalData(startDate, endDate);
} }
};
init();
return () => { return () => {
if (unsubscribe) unsubscribe(); if (unsubscribe) {
stopRealtimeUpdates(); unsubscribe(); // Вызываем функцию отписки
}
if (mode === 'realtime') {
stopRealtimeUpdates(); // Дополнительная очистка
}
}; };
}, [mode, metricName, device, source_id]); }, [mode, metricName, device, source_id, JSON.stringify(filters)]); // Добавляем JSON.stringify для фильтров
const metaInfo = [ const metaInfo = [
metricMeta.instance && `Instance: ${metricMeta.instance}`, metricMeta.instance && `Instance: ${metricMeta.instance}`,
@ -209,7 +263,7 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => {
].filter(Boolean).join(' | '); ].filter(Boolean).join(' | ');
return ( return (
<div style={{ position: 'relative' }}> <div>
<div style={{ marginBottom: 16 }}> <div style={{ marginBottom: 16 }}>
<Radio.Group <Radio.Group
value={mode} value={mode}
@ -231,10 +285,15 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => {
/> />
)} )}
{mode === 'realtime' && ( {mode === 'realtime' && isLiveUpdating && (
<Tag color={isLiveUpdating ? 'green' : 'red'}> <Button
{isLiveUpdating ? 'Обновление в реальном времени' : 'Режим реального времени остановлен'} type="primary"
</Tag> danger
onClick={() => setMode('historical')}
style={{ marginTop: 10 }}
>
Остановить обновление
</Button>
)} )}
</div> </div>
@ -259,26 +318,28 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => {
</MuiTooltip> </MuiTooltip>
{isLoading ? ( {isLoading ? (
<div style={{ height: chartHeight, display: 'flex', justifyContent: 'center', alignItems: 'center' }}> <div>Загрузка графика...</div>
<Spin size="large" tip="Загрузка данных..." />
</div>
) : error ? ( ) : error ? (
<div style={{ color: 'red', padding: 20 }}>Ошибка: {error}</div> <div>Ошибка: {error}</div>
) : rawData.length === 0 ? ( ) : chartData.length === 0 ? (
<div style={{ padding: 20 }}>Нет данных для метрики: {metricName}</div> <div>Нет данных для метрики: {metricName}</div>
) : ( ) : (
<> <>
<LineChartComponent <LineChartComponent
data={rawData} data={chartData}
title={title} title={title}
description={description} description={description}
multipleLines={multipleLines}
lineKey={lineKey}
metaInfo={metaInfo} metaInfo={metaInfo}
height={chartHeight} height={chartHeight}
additionalFilters={{
device,
source_id
}}
ranges={ranges} ranges={ranges}
/> />
{showLogs && <StatusLogTable logs={statusLogs} />} {showLogs && (
<StatusLogTable logs={statusLogs} />
)}
</> </>
)} )}
</Box> </Box>

View File

@ -43,6 +43,16 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
return `${metricName}${filterParts.length ? `?${filterParts.join('&')}` : ''}`; return `${metricName}${filterParts.length ? `?${filterParts.join('&')}` : ''}`;
}; };
const getStatusFromRanges = (value, ranges) => {
if (!ranges || ranges.length === 0) return 1;
for (const r of ranges) {
if (value >= r.min && value <= r.max) {
return r.status;
}
}
return 1;
};
const formatMetricData = (dataArray) => { const formatMetricData = (dataArray) => {
if (!Array.isArray(dataArray)) { if (!Array.isArray(dataArray)) {
console.error('Expected array in formatMetricData, got:', typeof dataArray); console.error('Expected array in formatMetricData, got:', typeof dataArray);
@ -59,7 +69,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
...item, ...item,
timestamp: Number(item.timestamp), timestamp: Number(item.timestamp),
value: parseFloat(item.value), value: parseFloat(item.value),
status: parseInt(item.status || '0'), status: getStatusFromRanges(parseFloat(item.value), ranges),
name: item.__name__ || metricName, name: item.__name__ || metricName,
device: item.device?.trim() || null, device: item.device?.trim() || null,
source_id: item.source_id || null, source_id: item.source_id || null,