переделал график, теперь он показывает корректные данные за диапазон
parent
a01d0972f5
commit
5613fcf205
30
src/App.css
30
src/App.css
|
|
@ -42,34 +42,4 @@
|
||||||
|
|
||||||
.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; /* Ползунок + фон */
|
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 = [];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue