переделал график, теперь он показывает корректные данные за диапазон

pull/8/head
DmitriyA 2025-03-07 09:02:32 -05:00
parent a01d0972f5
commit 5613fcf205
4 changed files with 105 additions and 256 deletions

View File

@ -43,33 +43,3 @@
.read-the-docs {
color: #888;
}
/* Глобальный стиль для WebKit-браузеров (Chrome, Edge, Safari) */
::-webkit-scrollbar {
width: 10px; /* Толщина вертикального скролла */
height: 10px; /* Толщина горизонтального скролла */
}
/* Фон скроллбара */
::-webkit-scrollbar-track {
background: #f1f1f1; /* Цвет фона */
border-radius: 10px; /* Скругление углов */
}
/* Ползунок */
::-webkit-scrollbar-thumb {
background: #3d74c7; /* Основной цвет */
border-radius: 10px; /* Скругляем края */
border: 2px solid #f1f1f1; /* Белая обводка */
}
/* Эффект при наведении */
::-webkit-scrollbar-thumb:hover {
background: #2b5aa5; /* Чуть темнее при наведении */
}
/* Глобальный стиль для Firefox */
* {
scrollbar-width: thin; /* Делаем тонким */
scrollbar-color: #3d74c7 #f1f1f1; /* Ползунок + фон */
}

View File

@ -1,12 +1,12 @@
import React from 'react';
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Line, ResponsiveContainer, Brush } from 'recharts';
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Line, ResponsiveContainer } from 'recharts';
const LineChartComponent = ({ chartData, metricName, metricType, colors, description, isLongRange }) => {
const LineChartComponent = ({ chartData, metricName, metricType, colors, description }) => {
// Создаем массив уникальных временных меток
const allTimes = Object.values(chartData)
.flat()
.map(point => point.time)
.filter((time, index, self) => self.indexOf(time) === index); // Убираем дубликаты
.filter((time, index, self) => self.indexOf(time) === index);
// Формируем данные для графика
const data = allTimes.map(time => {
@ -18,14 +18,17 @@ const LineChartComponent = ({ chartData, metricName, metricType, colors, descrip
return point;
});
console.log('Processed Data:', data); // Логируем данные для графика
// Кастомный Tooltip для отображения значения
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div className="custom-tooltip">
<p>{`${payload[0].value}`}</p>
<div className="custom-tooltip" style={{ padding: '10px' }}>
<p>{`Время: ${label}`}</p> {/* Время из label */}
{payload.map((entry, index) => (
<p key={index} style={{}}>
{`Значение: ${entry.value}`} {/* Имя и значение из payload */}
</p>
))}
</div>
);
}
@ -41,7 +44,7 @@ const LineChartComponent = ({ chartData, metricName, metricType, colors, descrip
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis />
<Tooltip content={<CustomTooltip />} />
<Tooltip content={<CustomTooltip />} /> {/* Подключаем кастомный Tooltip */}
<Legend />
{Object.keys(chartData).map((key, index) => (
<Line
@ -52,15 +55,6 @@ const LineChartComponent = ({ chartData, metricName, metricType, colors, descrip
name={key}
/>
))}
{isLongRange && ( // Добавляем Brush только для длительных периодов
<Brush
dataKey="time"
height={30}
stroke="#8884d8"
startIndex={0} // Начальный индекс
endIndex={data.length - 1} // Конечный индекс
/>
)}
</LineChart>
</ResponsiveContainer>
</div>

View File

@ -1,258 +1,141 @@
import React, { useEffect, useState, useRef } from 'react';
import axios from 'axios';
import LineChartComponent from './Components/LineChartComponent';
import BarChartComponent from './Components/BarChartComponent';
import ScatterChartComponent from './Components/ScatterChartComponent';
import DatePicker from 'react-datepicker';
import '../Style/DatePicker.css';
const MAX_POINTS = 20; // Ограничение точек на графике
const COLORS = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850']; // Фиксированные цвета для линий
// Компонент для выбора временного диапазона
const TimeRangeSelector = ({ onRangeChange }) => {
return (
<div style={{ marginBottom: '20px' }}>
<button onClick={() => onRangeChange('1h')}>Последний час</button>
<button onClick={() => onRangeChange('24h')}>Последние сутки</button>
<button onClick={() => onRangeChange('2w')}>Две недели</button>
</div>
);
};
// Компонент для выбора произвольного диапазона дат
const DateRangeSelector = ({ onDateChange }) => {
const [startDate, setStartDate] = useState(new Date());
const [endDate, setEndDate] = useState(new Date());
const handleDateChange = (dates) => {
const [start, end] = dates;
setStartDate(start);
setEndDate(end);
onDateChange({ start, end });
};
return (
<div style={{ marginBottom: '20px', position: 'relative' }}>
<DatePicker
selectsRange
startDate={startDate}
endDate={endDate}
onChange={handleDateChange}
isClearable
dateFormat="yyyy-MM-dd"
popperPlacement="right-start" // Открывать справа от поля ввода
popperModifiers={[
{
name: 'offset',
options: {
offset: [0, 10], // Смещение календаря относительно поля ввода
},
},
]}
/>
</div>
);
};
// Список временных диапазонов и интервалов обновления
const TIME_RANGES = [
{ label: '1 минута', value: 60, interval: 3000 },
{ label: '5 минут', value: 300, interval: 15000 },
{ label: '30 минут', value: 1800, interval: 90000 },
{ label: '1 час', value: 3600, interval: 180000 },
{ label: '3 часа', value: 10800, interval: 540000 },
{ label: '6 часов', value: 21600, interval: 1080000 },
{ label: '12 часов', value: 43200, interval: 2160000 },
{ label: '24 часа', value: 86400, interval: 4320000 },
{ label: '2 дня', value: 172800, interval: 8640000 },
{ label: '7 дней', value: 604800, interval: 30240000 },
{ label: '30 дней', value: 2592000, interval: 129600000 },
{ label: '90 дней', value: 7776000, interval: 388800000 },
{ label: '6 месяцев', value: 15552000, interval: 777600000 },
{ label: '9 месяцев', value: 23328000, interval: 1166400000 },
{ label: '1 год', value: 31536000, interval: 1576800000 },
];
const PrometheusChart = ({ metricName }) => {
const [chartData, setChartData] = useState({});
const [metricType, setMetricType] = useState('');
const [metricDescription, setMetricDescription] = useState('');
const [timeRange, setTimeRange] = useState('24h'); // По умолчанию выбран диапазон "24 часа"
const [customRange, setCustomRange] = useState({ start: null, end: null });
const [selectedRange, setSelectedRange] = useState(TIME_RANGES[0]); // По умолчанию 1 минута
const intervalRef = useRef(null);
const isLongRange = (range) => {
return range === '2w' || range === 'custom'; // "2w" две недели, "custom" кастомный диапазон
};
const fetchData = async (range, customStart, customEnd) => {
const fetchData = async () => {
try {
const end = customEnd ? Math.floor(customEnd.getTime() / 1000) : Math.floor(Date.now() / 1000);
let start;
const end = Math.floor(Date.now() / 1000);
const start = end - selectedRange.value;
if (customStart) {
start = Math.floor(customStart.getTime() / 1000);
} else {
switch (range) {
case '1h':
start = end - 60 * 60; // 1 час назад
break;
case '24h':
start = end - 24 * 60 * 60; // 24 часа назад
break;
case '2w':
start = end - 14 * 24 * 60 * 60; // 2 недели назад
break;
default:
start = end - 24 * 60 * 60; // По умолчанию 24 часа
}
}
// Динамический шаг (чем больше диапазон, тем больше шаг)
let step;
if (selectedRange.value <= 3600) step = 5; // 1 час и меньше 5 сек
else if (selectedRange.value <= 21600) step = 30; // 1-6 часов 30 сек
else if (selectedRange.value <= 86400) step = 120; // 6-24 часа 2 минуты
else step = 300; // > 24 часов 5 минут
const step = range === '2w' ? 3600 : 60; // Для двух недель увеличиваем шаг до 1 часа
console.log(`Запрашиваем данные с шагом ${step} сек`);
const response = await axios.get(`http://192.168.2.39:3000/metrics`, {
params: {
metric: metricName,
start,
end,
step,
},
params: { metric: metricName, start, end, step },
});
const result = response.data;
// Проверяем структуру данных
let metrics;
if (Array.isArray(result)) {
metrics = result;
} else if (result.data && Array.isArray(result.data)) {
metrics = result.data;
} else {
throw new Error('Invalid data format');
}
let metrics = Array.isArray(result) ? result : result.data || [];
if (!Array.isArray(metrics) || metrics.length === 0) {
throw new Error('No metrics data available');
console.warn('No metrics data available, filling with empty values.');
metrics = [];
}
const type = metrics[0].type;
setMetricType(type);
// 1. Генерация временных точек с учетом диапазона
const timePoints = [];
for (let t = start; t <= end; t += step) {
const date = new Date(t * 1000);
const formattedTime = selectedRange.value > 86400
? date.toLocaleString([], { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
// Устанавливаем описание метрики
setMetricDescription(metrics[0].description);
// Очищаем предыдущие данные
setChartData({});
if (type === 'summary') {
// Обработка данных для summary
const newData = metrics.map(m => ({
instance: m.instance,
quantile: m.quantile,
value: m.value
}));
// Группируем данные по instance
const groupedData = newData.reduce((acc, point) => {
if (!acc[point.instance]) {
acc[point.instance] = [];
}
acc[point.instance].push(point);
return acc;
}, {});
setChartData(groupedData);
} else {
// Обработка данных для counter, gauge, unknown
const newDataPoints = metrics.map(m => ({
time: new Date(m.timestamp).toLocaleTimeString(),
value: m.value,
instance: m.instance,
device: m.device || m.scrape_job, // Используем device или scrape_job
}));
// Группируем данные по instance и device/scrape_job
const updatedData = {};
newDataPoints.forEach(point => {
const key = `${point.instance}-${point.device}`; // Уникальный ключ
if (!updatedData[key]) {
updatedData[key] = [];
}
updatedData[key].push({
time: point.time,
value: point.value,
});
// Ограничиваем количество точек до MAX_POINTS
if (updatedData[key].length > MAX_POINTS) {
updatedData[key] = updatedData[key].slice(-MAX_POINTS); // Оставляем последние MAX_POINTS точек
}
});
setChartData(updatedData);
timePoints.push(formattedTime);
}
// 2. Обработка данных
const updatedData = {};
metrics.forEach(m => {
const date = new Date(m.timestamp);
const formattedTime = selectedRange.value > 86400
? date.toLocaleString([], { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const key = `${m.instance}-${m.device || m.scrape_job}`;
if (!updatedData[key]) updatedData[key] = {};
updatedData[key][formattedTime] = m.value;
});
// 3. Заполнение пропусков
const chartData = {};
Object.keys(updatedData).forEach(key => {
chartData[key] = timePoints.map(time => ({
time,
value: updatedData[key][time] ?? null,
}));
});
setChartData(chartData);
} catch (error) {
console.error('Error fetching metrics:', error);
console.error('Ошибка при загрузке метрик:', error);
}
};
useEffect(() => {
fetchData(timeRange, customRange.start, customRange.end); // Первоначальная загрузка данных
if (!isLongRange(timeRange)) { // Обновляем только для коротких диапазонов
intervalRef.current = setInterval(() => {
fetchData(timeRange, customRange.start, customRange.end);
}, 5000); // Обновляем каждые 5 секунд
}
useEffect(() => {
fetchData(); // Первоначальная загрузка данных
intervalRef.current = setInterval(() => {
fetchData();
}, selectedRange.interval); // Обновляем с выбранным интервалом
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current); // Очищаем интервал при размонтировании
}
};
}, [metricName, timeRange, customRange]);
}, [metricName, selectedRange]); // Зависимость от metricName и selectedRange
const handleRangeChange = (range) => {
setTimeRange(range);
setCustomRange({ start: null, end: null });
if (!isLongRange(range)) {
clearInterval(intervalRef.current); // Останавливаем обновление
}
};
const handleDateChange = ({ start, end }) => {
setCustomRange({ start, end });
setTimeRange('custom'); // Устанавливаем кастомный диапазон
const handleRangeChange = (event) => {
const selectedValue = event.target.value;
const range = TIME_RANGES.find(range => range.value === parseInt(selectedValue, 10));
setSelectedRange(range);
};
if (!Object.keys(chartData).length) return <p>Loading...</p>;
const renderChart = () => {
switch (metricType) {
case 'counter':
case 'gauge':
return (
<LineChartComponent
chartData={chartData}
metricName={metricName}
metricType={metricType}
colors={COLORS}
description={metricDescription}
isLongRange={isLongRange(timeRange)} // Передаем флаг
/>
);
case 'summary':
return (
<BarChartComponent
chartData={chartData}
metricName={metricName}
metricType={metricType}
colors={COLORS}
/>
);
case 'unknown':
return (
<ScatterChartComponent
chartData={chartData}
metricName={metricName}
metricType={metricType}
colors={COLORS}
/>
);
default:
return <p>Unsupported metric type</p>;
}
};
return (
<div>
<TimeRangeSelector onRangeChange={handleRangeChange} />
<DateRangeSelector onDateChange={handleDateChange} />
{renderChart()}
<div>
<label htmlFor="time-range">Выберите временной диапазон: </label>
<select id="time-range" value={selectedRange.value} onChange={handleRangeChange}>
{TIME_RANGES.map(range => (
<option key={range.value} value={range.value}>{range.label}</option>
))}
</select>
</div>
<LineChartComponent
chartData={chartData}
metricName={metricName}
metricType={metricType}
colors={COLORS}
description={metricDescription}
/>
</div>
);
};

View File

@ -7,6 +7,8 @@ const getMetricName = (id) => {
return `zvks_apiforsnmp_measure_${id}`;
};
//!!!!!!!!!!Пофиксить вкладуи с eth4, во всех eth 1-4 открывается именно 4 !!!!!!!!!!!!!
// Функция для рекурсивного сбора всех id потомков
const getAllChildIds = (node) => {
let ids = [];