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

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

View File

@ -1,258 +1,141 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import axios from 'axios'; import axios from 'axios';
import LineChartComponent from './Components/LineChartComponent'; 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 MAX_POINTS = 20; // Ограничение точек на графике
const COLORS = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850']; // Фиксированные цвета для линий const COLORS = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850']; // Фиксированные цвета для линий
// Компонент для выбора временного диапазона // Список временных диапазонов и интервалов обновления
const TimeRangeSelector = ({ onRangeChange }) => { const TIME_RANGES = [
return ( { label: '1 минута', value: 60, interval: 3000 },
<div style={{ marginBottom: '20px' }}> { label: '5 минут', value: 300, interval: 15000 },
<button onClick={() => onRangeChange('1h')}>Последний час</button> { label: '30 минут', value: 1800, interval: 90000 },
<button onClick={() => onRangeChange('24h')}>Последние сутки</button> { label: '1 час', value: 3600, interval: 180000 },
<button onClick={() => onRangeChange('2w')}>Две недели</button> { label: '3 часа', value: 10800, interval: 540000 },
</div> { 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 },
const DateRangeSelector = ({ onDateChange }) => { { label: '30 дней', value: 2592000, interval: 129600000 },
const [startDate, setStartDate] = useState(new Date()); { label: '90 дней', value: 7776000, interval: 388800000 },
const [endDate, setEndDate] = useState(new Date()); { label: '6 месяцев', value: 15552000, interval: 777600000 },
{ label: '9 месяцев', value: 23328000, interval: 1166400000 },
const handleDateChange = (dates) => { { label: '1 год', value: 31536000, interval: 1576800000 },
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 PrometheusChart = ({ metricName }) => { const PrometheusChart = ({ metricName }) => {
const [chartData, setChartData] = useState({}); const [chartData, setChartData] = useState({});
const [metricType, setMetricType] = useState(''); const [metricType, setMetricType] = useState('');
const [metricDescription, setMetricDescription] = useState(''); const [metricDescription, setMetricDescription] = useState('');
const [timeRange, setTimeRange] = useState('24h'); // По умолчанию выбран диапазон "24 часа" const [selectedRange, setSelectedRange] = useState(TIME_RANGES[0]); // По умолчанию 1 минута
const [customRange, setCustomRange] = useState({ start: null, end: null });
const intervalRef = useRef(null); const intervalRef = useRef(null);
const isLongRange = (range) => { const fetchData = async () => {
return range === '2w' || range === 'custom'; // "2w" две недели, "custom" кастомный диапазон
};
const fetchData = async (range, customStart, customEnd) => {
try { try {
const end = customEnd ? Math.floor(customEnd.getTime() / 1000) : Math.floor(Date.now() / 1000); const end = Math.floor(Date.now() / 1000);
let start; const start = end - selectedRange.value;
if (customStart) { // Динамический шаг (чем больше диапазон, тем больше шаг)
start = Math.floor(customStart.getTime() / 1000); let step;
} else { if (selectedRange.value <= 3600) step = 5; // 1 час и меньше 5 сек
switch (range) { else if (selectedRange.value <= 21600) step = 30; // 1-6 часов 30 сек
case '1h': else if (selectedRange.value <= 86400) step = 120; // 6-24 часа 2 минуты
start = end - 60 * 60; // 1 час назад else step = 300; // > 24 часов 5 минут
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 часа
}
}
const step = range === '2w' ? 3600 : 60; // Для двух недель увеличиваем шаг до 1 часа console.log(`Запрашиваем данные с шагом ${step} сек`);
const response = await axios.get(`http://192.168.2.39:3000/metrics`, { const response = await axios.get(`http://192.168.2.39:3000/metrics`, {
params: { params: { metric: metricName, start, end, step },
metric: metricName,
start,
end,
step,
},
}); });
const result = response.data; const result = response.data;
let metrics = Array.isArray(result) ? result : result.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');
}
if (!Array.isArray(metrics) || metrics.length === 0) { 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; // 1. Генерация временных точек с учетом диапазона
setMetricType(type); 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' });
// Устанавливаем описание метрики timePoints.push(formattedTime);
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);
} }
// 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) { } catch (error) {
console.error('Error fetching metrics:', error); console.error('Ошибка при загрузке метрик:', error);
} }
}; };
useEffect(() => {
fetchData(timeRange, customRange.start, customRange.end); // Первоначальная загрузка данных
if (!isLongRange(timeRange)) { // Обновляем только для коротких диапазонов useEffect(() => {
intervalRef.current = setInterval(() => { fetchData(); // Первоначальная загрузка данных
fetchData(timeRange, customRange.start, customRange.end);
}, 5000); // Обновляем каждые 5 секунд intervalRef.current = setInterval(() => {
} fetchData();
}, selectedRange.interval); // Обновляем с выбранным интервалом
return () => { return () => {
if (intervalRef.current) { if (intervalRef.current) {
clearInterval(intervalRef.current); // Очищаем интервал при размонтировании clearInterval(intervalRef.current); // Очищаем интервал при размонтировании
} }
}; };
}, [metricName, timeRange, customRange]); }, [metricName, selectedRange]); // Зависимость от metricName и selectedRange
const handleRangeChange = (range) => { const handleRangeChange = (event) => {
setTimeRange(range); const selectedValue = event.target.value;
setCustomRange({ start: null, end: null }); const range = TIME_RANGES.find(range => range.value === parseInt(selectedValue, 10));
setSelectedRange(range);
if (!isLongRange(range)) {
clearInterval(intervalRef.current); // Останавливаем обновление
}
};
const handleDateChange = ({ start, end }) => {
setCustomRange({ start, end });
setTimeRange('custom'); // Устанавливаем кастомный диапазон
}; };
if (!Object.keys(chartData).length) return <p>Loading...</p>; 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 ( return (
<div> <div>
<TimeRangeSelector onRangeChange={handleRangeChange} /> <div>
<DateRangeSelector onDateChange={handleDateChange} /> <label htmlFor="time-range">Выберите временной диапазон: </label>
{renderChart()} <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> </div>
); );
}; };

View File

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