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

View File

@ -43,6 +43,16 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
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) => {
if (!Array.isArray(dataArray)) {
console.error('Expected array in formatMetricData, got:', typeof dataArray);
@ -59,7 +69,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
...item,
timestamp: Number(item.timestamp),
value: parseFloat(item.value),
status: parseInt(item.status || '0'),
status: getStatusFromRanges(parseFloat(item.value), ranges),
name: item.__name__ || metricName,
device: item.device?.trim() || null,
source_id: item.source_id || null,