added complex variables
parent
421d95565c
commit
205ddc71e0
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue