Compare commits

..

No commits in common. "0ce3312306a330bc751827623e8a063ee1a925b6" and "f2abd66f8781d190882a1df08c1c92a0c441bd78" have entirely different histories.

4 changed files with 252 additions and 101 deletions

View File

@ -43,3 +43,33 @@
.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 } from 'recharts'; import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Line, ResponsiveContainer, Brush } from 'recharts';
const LineChartComponent = ({ chartData, metricName, metricType, colors, description }) => { const LineChartComponent = ({ chartData, metricName, metricType, colors, description, isLongRange }) => {
// Создаем массив уникальных временных меток // Создаем массив уникальных временных меток
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,17 +18,14 @@ 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" style={{ padding: '10px' }}> <div className="custom-tooltip">
<p>{`Время: ${label}`}</p> {/* Время из label */} <p>{`${payload[0].value}`}</p>
{payload.map((entry, index) => (
<p key={index} style={{}}>
{`Значение: ${entry.value}`} {/* Имя и значение из payload */}
</p>
))}
</div> </div>
); );
} }
@ -44,7 +41,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 */} <Tooltip content={<CustomTooltip />} />
<Legend /> <Legend />
{Object.keys(chartData).map((key, index) => ( {Object.keys(chartData).map((key, index) => (
<Line <Line
@ -55,6 +52,15 @@ 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,141 +1,258 @@
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 TIME_RANGES = [ const TimeRangeSelector = ({ onRangeChange }) => {
{ label: '1 минута', value: 60, interval: 3000 }, return (
{ label: '5 минут', value: 300, interval: 15000 }, <div style={{ marginBottom: '20px' }}>
{ label: '30 минут', value: 1800, interval: 90000 }, <button onClick={() => onRangeChange('1h')}>Последний час</button>
{ label: '1 час', value: 3600, interval: 180000 }, <button onClick={() => onRangeChange('24h')}>Последние сутки</button>
{ label: '3 часа', value: 10800, interval: 540000 }, <button onClick={() => onRangeChange('2w')}>Две недели</button>
{ label: '6 часов', value: 21600, interval: 1080000 }, </div>
{ 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 }, const DateRangeSelector = ({ onDateChange }) => {
{ label: '90 дней', value: 7776000, interval: 388800000 }, const [startDate, setStartDate] = useState(new Date());
{ label: '6 месяцев', value: 15552000, interval: 777600000 }, const [endDate, setEndDate] = useState(new Date());
{ label: '9 месяцев', value: 23328000, interval: 1166400000 },
{ label: '1 год', value: 31536000, interval: 1576800000 }, 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 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 [selectedRange, setSelectedRange] = useState(TIME_RANGES[0]); // По умолчанию 1 минута const [timeRange, setTimeRange] = useState('24h'); // По умолчанию выбран диапазон "24 часа"
const [customRange, setCustomRange] = useState({ start: null, end: null });
const intervalRef = useRef(null); const intervalRef = useRef(null);
const fetchData = async () => { const isLongRange = (range) => {
return range === '2w' || range === 'custom'; // "2w" две недели, "custom" кастомный диапазон
};
const fetchData = async (range, customStart, customEnd) => {
try { try {
const end = Math.floor(Date.now() / 1000); const end = customEnd ? Math.floor(customEnd.getTime() / 1000) : Math.floor(Date.now() / 1000);
const start = end - selectedRange.value; let start;
// Динамический шаг (чем больше диапазон, тем больше шаг) if (customStart) {
let step; start = Math.floor(customStart.getTime() / 1000);
if (selectedRange.value <= 3600) step = 5; // 1 час и меньше 5 сек } else {
else if (selectedRange.value <= 21600) step = 30; // 1-6 часов 30 сек switch (range) {
else if (selectedRange.value <= 86400) step = 120; // 6-24 часа 2 минуты case '1h':
else step = 300; // > 24 часов 5 минут 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 часа
}
}
console.log(`Запрашиваем данные с шагом ${step} сек`); const step = range === '2w' ? 3600 : 60; // Для двух недель увеличиваем шаг до 1 часа
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: { metric: metricName, start, end, step }, params: {
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) {
console.warn('No metrics data available, filling with empty values.'); throw new Error('No metrics data available');
metrics = [];
} }
// 1. Генерация временных точек с учетом диапазона const type = metrics[0].type;
const timePoints = []; setMetricType(type);
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);
// 2. Обработка данных // Очищаем предыдущие данные
const updatedData = {}; setChartData({});
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 (type === 'summary') {
if (!updatedData[key]) updatedData[key] = {}; // Обработка данных для summary
updatedData[key][formattedTime] = m.value; const newData = metrics.map(m => ({
}); instance: m.instance,
quantile: m.quantile,
// 3. Заполнение пропусков value: m.value
const chartData = {};
Object.keys(updatedData).forEach(key => {
chartData[key] = timePoints.map(time => ({
time,
value: updatedData[key][time] ?? null,
})); }));
});
setChartData(chartData); // Группируем данные по 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);
}
} catch (error) { } catch (error) {
console.error('Ошибка при загрузке метрик:', error); console.error('Error fetching metrics:', error);
} }
}; };
useEffect(() => { useEffect(() => {
fetchData(); // Первоначальная загрузка данных fetchData(timeRange, customRange.start, customRange.end); // Первоначальная загрузка данных
intervalRef.current = setInterval(() => { if (!isLongRange(timeRange)) { // Обновляем только для коротких диапазонов
fetchData(); intervalRef.current = setInterval(() => {
}, selectedRange.interval); // Обновляем с выбранным интервалом fetchData(timeRange, customRange.start, customRange.end);
}, 5000); // Обновляем каждые 5 секунд
}
return () => { return () => {
if (intervalRef.current) { if (intervalRef.current) {
clearInterval(intervalRef.current); // Очищаем интервал при размонтировании clearInterval(intervalRef.current); // Очищаем интервал при размонтировании
} }
}; };
}, [metricName, selectedRange]); // Зависимость от metricName и selectedRange }, [metricName, timeRange, customRange]);
const handleRangeChange = (event) => { const handleRangeChange = (range) => {
const selectedValue = event.target.value; setTimeRange(range);
const range = TIME_RANGES.find(range => range.value === parseInt(selectedValue, 10)); setCustomRange({ start: null, end: null });
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>
<div> <TimeRangeSelector onRangeChange={handleRangeChange} />
<label htmlFor="time-range">Выберите временной диапазон: </label> <DateRangeSelector onDateChange={handleDateChange} />
<select id="time-range" value={selectedRange.value} onChange={handleRangeChange}> {renderChart()}
{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,8 +7,6 @@ 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 = [];