переделал график, теперь он показывает корректные данные за диапазон
parent
a01d0972f5
commit
5613fcf205
30
src/App.css
30
src/App.css
|
|
@ -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; /* Ползунок + фон */
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ const getMetricName = (id) => {
|
|||
return `zvks_apiforsnmp_measure_${id}`;
|
||||
};
|
||||
|
||||
//!!!!!!!!!!Пофиксить вкладуи с eth4, во всех eth 1-4 открывается именно 4 !!!!!!!!!!!!!
|
||||
|
||||
// Функция для рекурсивного сбора всех id потомков
|
||||
const getAllChildIds = (node) => {
|
||||
let ids = [];
|
||||
|
|
|
|||
Loading…
Reference in New Issue