diff --git a/src/Charts/Components/LineChartComponent.jsx b/src/Charts/Components/LineChartComponent.jsx
index 53d859f..caca080 100755
--- a/src/Charts/Components/LineChartComponent.jsx
+++ b/src/Charts/Components/LineChartComponent.jsx
@@ -11,28 +11,59 @@ const LineChartComponent = ({
const [selectionArea, setSelectionArea] = useState(null);
const [isSelecting, setIsSelecting] = useState(false);
const chartRef = useRef(null);
- const containerRef = useRef(null);
- const allTimes = Object.values(chartData)
+ const allTimestamps = Object.values(chartData)
.flat()
- .map(point => point.time)
- .filter((time, index, self) => self.indexOf(time) === index);
+ .map(point => point.timestamp)
+ .filter((timestamp, index, self) => self.indexOf(timestamp) === index)
+ .sort((a, b) => a - b);
+
+ const data = allTimestamps.map(timestamp => {
+ const point = { timestamp };
+
+ const firstPoint = Object.values(chartData)
+ .flat()
+ .find(p => p.timestamp === timestamp);
+
+ if (firstPoint) {
+ point.time = firstPoint.time;
+ point.fullTime = firstPoint.fullTime;
+ }
- const data = allTimes.map(time => {
- const point = { time };
Object.keys(chartData).forEach(key => {
- const instanceData = chartData[key].find(p => p.time === time);
+ const instanceData = chartData[key].find(p => p.timestamp === timestamp);
point[key] = instanceData ? instanceData.value : null;
});
+
return point;
- }).sort((a, b) => {
- const timeA = chartData[Object.keys(chartData)[0]].find(d => d.time === a.time)?.timestamp;
- const timeB = chartData[Object.keys(chartData)[0]].find(d => d.time === b.time)?.timestamp;
- return timeA - timeB;
});
const displayData = filteredData || data;
+ const instanceKeys = displayData.length
+ ? Object.keys(displayData[0]).filter(k => !['timestamp', 'time', 'fullTime'].includes(k))
+ : [];
+
+ // Функция для определения оптимального формата времени в зависимости от диапазона
+ const getTimeFormat = () => {
+ if (!data.length) return 'HH:mm:ss';
+
+ const first = data[0].timestamp;
+ const last = data[data.length - 1].timestamp;
+ const range = last - first;
+
+ // Если диапазон больше 24 часов - показываем дату
+ if (range > 86400000) {
+ return 'dd.MM HH:mm';
+ }
+ // Если больше 1 часа - показываем часы и минуты
+ if (range > 3600000) {
+ return 'HH:mm';
+ }
+ // Для коротких диапазонов - показываем время с секундами
+ return 'HH:mm:ss';
+ };
+
useEffect(() => {
const handleSelectStart = (e) => {
if (isSelecting) {
@@ -40,44 +71,66 @@ const LineChartComponent = ({
}
};
+
document.addEventListener('selectstart', handleSelectStart);
return () => document.removeEventListener('selectstart', handleSelectStart);
}, [isSelecting]);
const handleMouseDown = (e) => {
- if (!e || !e.activeLabel) return;
+ if (!e) return;
+
+ // Получаем индекс точки по координатам
+ const activeIndex = e.activeTooltipIndex;
+ if (activeIndex === undefined || activeIndex < 0 || activeIndex >= data.length) return;
+
setIsSelecting(true);
- setSelectionArea({ start: e.activeLabel, end: null });
+ setSelectionArea({
+ start: data[activeIndex].timestamp,
+ end: null,
+ startIndex: activeIndex,
+ endIndex: null
+ });
};
const handleMouseMove = (e) => {
- if (!selectionArea?.start || !e?.activeLabel) return;
- setSelectionArea(prev => ({ ...prev, end: e.activeLabel }));
+ if (!isSelecting || !selectionArea?.start || !e) return;
+
+ const activeIndex = e.activeTooltipIndex;
+ if (activeIndex === undefined || activeIndex < 0 || activeIndex >= data.length) return;
+
+ setSelectionArea(prev => ({
+ ...prev,
+ end: data[activeIndex].timestamp,
+ endIndex: activeIndex
+ }));
};
const handleMouseUp = () => {
- setIsSelecting(false);
-
- if (!selectionArea?.start || !selectionArea?.end) {
+ if (!isSelecting || !selectionArea?.start || !selectionArea?.end) {
+ setIsSelecting(false);
setSelectionArea(null);
return;
}
- const startIndex = data.findIndex(point => point.time === selectionArea.start);
- const endIndex = data.findIndex(point => point.time === selectionArea.end);
+ const startIndex = Math.min(selectionArea.startIndex, selectionArea.endIndex);
+ const endIndex = Math.max(selectionArea.startIndex, selectionArea.endIndex);
- if (startIndex >= 0 && endIndex >= 0) {
- onRangeSelect({
- startIndex: Math.min(startIndex, endIndex),
- endIndex: Math.max(startIndex, endIndex)
- });
- }
+ // Нормализуем индексы к диапазону [0, 1] для родительского компонента
+ const normalizedStart = startIndex / (data.length - 1);
+ const normalizedEnd = endIndex / (data.length - 1);
+ onRangeSelect({
+ startIndex: normalizedStart,
+ endIndex: normalizedEnd
+ });
+
+ setIsSelecting(false);
setSelectionArea(null);
};
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
+ const currentPoint = data.find(point => point.timestamp === label);
return (
-
{`${label}`}
+
+ {currentPoint?.fullTime || new Date(label).toLocaleString('ru-RU')}
+
{payload.map((item, index) => (
{`Значение: ${item.value}`}
@@ -103,20 +158,7 @@ const LineChartComponent = ({
}
return (
-
-
-
+
{
+ const date = new Date(timestamp);
+ const format = getTimeFormat();
+
+ if (format === 'dd.MM HH:mm') {
+ return date.toLocaleString('ru-RU', {
+ day: '2-digit',
+ month: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ } else if (format === 'HH:mm') {
+ return date.toLocaleString('ru-RU', {
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ } else {
+ return date.toLocaleString('ru-RU', {
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit'
+ });
+ }
+ }}
/>
+
} />
- {Object.keys(chartData).map((instance, index) => (
+ {instanceKeys.map((instance, index) => (
{
const [selectedGraphRange, setSelectedGraphRange] = useState(null);
const [filteredData, setFilteredData] = useState(null);
const [isSelectingRange, setIsSelectingRange] = useState(false);
+ const [lastCustomRange, setLastCustomRange] = useState(null);
const intervalRef = useRef(null);
const socketRef = useRef(null);
+ const debounceRef = useRef(null);
const formatTime = useCallback((timestamp, rangeSeconds) => {
- const date = new Date(timestamp);
- if (rangeSeconds > 86400) {
- return {
- display: date.toLocaleString([], {
- day: '2-digit',
- month: '2-digit',
- year: '2-digit',
- hour: '2-digit',
- minute: '2-digit'
- }),
- timestamp: timestamp
- };
- }
+ const ts = typeof timestamp === 'number' ? timestamp : Date.now();
+ const date = new Date(ts);
+
+ // Определяем формат в зависимости от диапазона
+ const showFullDate = rangeSeconds > 86400; // больше суток
+
+ const timeOptions = {
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: false
+ };
+
+ const dateOptions = showFullDate ? {
+ month: '2-digit',
+ day: '2-digit',
+ ...timeOptions
+ } : timeOptions;
+
return {
- display: date.toLocaleTimeString([], {
+ display: date.toLocaleString('ru-RU', dateOptions),
+ fullDisplay: date.toLocaleString('ru-RU', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
hour: '2-digit',
minute: '2-digit',
- second: '2-digit'
+ second: '2-digit',
+ hour12: false
}),
- timestamp: timestamp
+ timestamp: ts
};
}, []);
const calculateStep = useCallback((start, end) => {
const range = end - start;
- if (range <= 3600) return 5;
- if (range <= 21600) return 30;
- if (range <= 86400) return 120;
- return 300;
+ if (range <= 60) return 1; // 1 мин
+ if (range <= 300) return 5; // 5 мин
+ if (range <= 1800) return 15; // 30 мин
+ if (range <= 3600) return 30; // 1 час
+ if (range <= 10800) return 60; // 3 часа
+ if (range <= 21600) return 120; // 6 часов
+ if (range <= 43200) return 300; // 12 часов
+ if (range <= 86400) return 600; // 24 часа
+ return 1800; // > 24 часов
}, []);
const fetchData = useCallback(() => {
-
+
if (isSelectingRange) return;
-
+
const now = Math.floor(Date.now() / 1000);
const start = now - selectedRange.value;
const end = now;
@@ -70,61 +88,54 @@ const PrometheusChart = ({ metricName }) => {
_t: Date.now()
});
}
- }, [metricName, selectedRange.value, isSelectingRange]);
-
- const groupBySecond = (points) => {
- const grouped = [];
- const timeMap = {};
-
- points.forEach(point => {
- const timeKey = Math.floor(point.timestamp / 1000);
- if (!timeMap[timeKey]) {
- timeMap[timeKey] = { ...point, count: 1 };
- grouped.push(timeMap[timeKey]);
- } else {
- timeMap[timeKey].value = (timeMap[timeKey].value * timeMap[timeKey].count + point.value) /
- (timeMap[timeKey].count + 1);
- timeMap[timeKey].count += 1;
- }
- });
-
- return grouped;
- };
+ }, [metricName, selectedRange.value, isSelectingRange]);
const processMetricsData = useCallback((response) => {
+ console.log('Processing metrics data:', response);
if (response.metric !== metricName) return;
-
+
const dataArray = Array.isArray(response.data) ? response.data : [response.data];
if (!dataArray.length) return;
-
+
setChartData(prev => {
const newData = { ...(prev || {}) };
-
+ const rangeSeconds = useCustomRange
+ ? (endDate.getTime() - startDate.getTime()) / 1000
+ : selectedRange.value;
+
dataArray.forEach(item => {
const instance = item.instance || 'default';
if (!newData[instance]) newData[instance] = [];
-
- const timestamp = item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000;
- const value = parseFloat(item.value);
-
- if (!newData[instance].some(p => p.timestamp === timestamp)) {
- newData[instance].push({
- time: formatTime(timestamp, selectedRange.value).display,
- value: value,
- timestamp: timestamp
- });
+
+ // Унифицированная конвертация timestamp
+ let timestamp;
+ if (typeof item.timestamp === 'number') {
+ // Определяем, в секундах или миллисекундах пришел timestamp
+ timestamp = item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000;
+ } else {
+ timestamp = Date.now();
}
+
+ const value = parseFloat(item.value);
+ const formattedTime = formatTime(timestamp, rangeSeconds);
+
+ newData[instance].push({
+ time: formattedTime.display,
+ fullTime: formattedTime.fullDisplay,
+ value: value,
+ timestamp: timestamp
+ });
});
-
+
+ // Сортируем и ограничиваем данные
Object.keys(newData).forEach(instance => {
- newData[instance] = groupBySecond(newData[instance])
+ newData[instance] = newData[instance]
.sort((a, b) => a.timestamp - b.timestamp)
.slice(-1000);
});
-
- return Object.keys(newData).length ? newData : prev;
+ return newData;
});
- }, [metricName, selectedRange.value, formatTime]);
+ }, [metricName, selectedRange.value, formatTime, useCustomRange, startDate, endDate]);
const setupWebSocket = useCallback(() => {
if (socketRef.current) {
@@ -178,26 +189,29 @@ const PrometheusChart = ({ metricName }) => {
const fetchCustomRangeData = useCallback(async () => {
const start = Math.floor(startDate.getTime() / 1000);
const end = Math.floor(endDate.getTime() / 1000);
- const step = calculateStep(start, end);
-
+ const rangeSeconds = end - start;
+
try {
const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/metrics`, {
params: {
metric: metricName,
start,
end,
- step
+ step: calculateStep(start, end)
}
});
-
- if (response.data) { // Изменили условие, так как бэкенд возвращает массив напрямую
+
+ if (response.data?.length) {
+ // Преобразуем данные перед передачей в processMetricsData
+ const processedData = response.data.map(item => ({
+ ...item,
+ timestamp: item.timestamp, // оставляем в секундах - processMetricsData конвертирует
+ value: item.value.toString()
+ }));
+
processMetricsData({
metric: metricName,
- data: response.data.map(item => ({
- ...item,
- timestamp: item.timestamp / 1000, // или item.timestamp если уже в секундах
- value: item.value.toString() // преобразуем в строку, как ожидает processMetricsData
- }))
+ data: processedData
});
}
} catch (error) {
@@ -207,63 +221,150 @@ const PrometheusChart = ({ metricName }) => {
const handleRangeChange = useCallback((event) => {
+ // Очищаем текущий интервал
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ intervalRef.current = null;
+ }
+
const selectedValue = event.target.value;
const range = TIME_RANGES.find(r => r.value === parseInt(selectedValue, 10));
-
+
setSelectedRange(range);
setUseCustomRange(false);
setChartData(null);
setSelectedGraphRange(null);
setFilteredData(null);
-
+
const now = new Date();
setEndDate(now);
setStartDate(new Date(now.getTime() - range.value * 1000));
-
- // Переподключение сокета при возврате к стандартным диапазонам
+
+ // Переподключение сокета
if (!socketRef.current?.connected) {
socketRef.current?.connect();
}
}, []);
const handleCustomRangeChange = useCallback(() => {
+ // Отключаем WebSocket соединение
+ if (socketRef.current?.connected) {
+ socketRef.current.disconnect();
+ setConnectionStatus('disconnected');
+ }
+
+ // Очищаем интервал обновления
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ intervalRef.current = null;
+ }
+
setUseCustomRange(true);
setChartData(null);
setSelectedGraphRange(null);
setFilteredData(null);
- }, []);
+ fetchCustomRangeData();
+ }, [fetchCustomRangeData]);
const handleResetZoom = useCallback(() => {
setSelectedGraphRange(null);
setFilteredData(null);
- setIsSelectingRange(false);
- fetchData();
- }, [fetchData]);
+ setIsSelectingRange(false);
+
+ if (useCustomRange) {
+ fetchCustomRangeData();
+ } else {
+ if (!socketRef.current?.connected) {
+ socketRef.current?.connect();
+ }
+ fetchData();
+ }
+
+ if (lastCustomRange) {
+ handleRangeSelect(lastCustomRange);
+ return;
+ }
+ }, [fetchData, fetchCustomRangeData, useCustomRange]);
+
+ const interpolateData = useCallback((data, targetPointCount) => {
+ if (!data || data.length < 2) return data;
+ if (data.length >= targetPointCount) return data;
+
+ const interpolated = [];
+ const step = (data.length - 1) / (targetPointCount - 1);
+
+ for (let i = 0; i < targetPointCount; i++) {
+ const index = i * step;
+ const lowerIndex = Math.floor(index);
+ const upperIndex = Math.ceil(index);
+
+ if (lowerIndex === upperIndex) {
+ interpolated.push(data[lowerIndex]);
+ continue;
+ }
+
+ const fraction = index - lowerIndex;
+ const interpolatedPoint = {};
+
+ Object.keys(data[lowerIndex]).forEach(key => {
+ if (key === 'timestamp') {
+ interpolatedPoint[key] = data[lowerIndex][key] +
+ fraction * (data[upperIndex][key] - data[lowerIndex][key]);
+
+ // Добавляем отображаемое время
+ const { display, fullDisplay } = formatTime(interpolatedPoint[key],
+ (endDate - startDate) / 1000);
+ interpolatedPoint.time = display;
+ interpolatedPoint.fullTime = fullDisplay;
+ } else if (typeof data[lowerIndex][key] === 'number') {
+ interpolatedPoint[key] = data[lowerIndex][key] +
+ fraction * (data[upperIndex][key] - data[lowerIndex][key]);
+ } else {
+ interpolatedPoint[key] = data[lowerIndex][key];
+ }
+ });
+
+ interpolated.push(interpolatedPoint);
+ }
+
+ return interpolated;
+ }, []);
const handleRangeSelect = useCallback((range) => {
- if (range) {
- // Начало выделения - останавливаем обновления
- setIsSelectingRange(true);
- setSelectedGraphRange(range);
-
- // Отключаем сокет
- if (socketRef.current?.connected) {
- socketRef.current.disconnect();
- }
- // Очищаем интервал
- if (intervalRef.current) {
- clearInterval(intervalRef.current);
- intervalRef.current = null;
- }
- } else {
- // Окончание выделения - возобновляем соединение
- setIsSelectingRange(false);
-
- if (!useCustomRange && socketRef.current && !socketRef.current.connected) {
- socketRef.current.connect();
- }
+ setLastCustomRange(range);
+ if (!range || !chartData) return;
+
+ setIsSelectingRange(true);
+ setSelectedGraphRange(range);
+
+ // Отключаем автоматические обновления
+ if (socketRef.current?.connected) {
+ socketRef.current.disconnect();
}
- }, [useCustomRange]);
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ intervalRef.current = null;
+ }
+
+ // Получаем все точки и сортируем по времени
+ const allPoints = Object.values(chartData).flat();
+ const sortedPoints = allPoints.sort((a, b) => a.timestamp - b.timestamp);
+
+ // Вычисляем абсолютные индексы
+ const startIndex = Math.floor(range.startIndex * (sortedPoints.length - 1));
+ const endIndex = Math.floor(range.endIndex * (sortedPoints.length - 1));
+
+ // Фильтруем точки по выбранному диапазону
+ const filtered = sortedPoints.slice(startIndex, endIndex + 1);
+
+ // Применяем интерполяцию только если точек меньше 100
+ const interpolated = filtered.length < 100 ?
+ interpolateData(filtered, Math.min(100, filtered.length * 3)) :
+ filtered;
+
+ setFilteredData(interpolated);
+ setIsSelectingRange(false);
+ }, [chartData, interpolateData, formatTime]);
useEffect(() => {
const socket = setupWebSocket();
@@ -273,17 +374,30 @@ const PrometheusChart = ({ metricName }) => {
};
}, [setupWebSocket]);
+ // Обновим useEffect для кастомного диапазона
useEffect(() => {
- if (useCustomRange) {
- if (socketRef.current?.connected) {
- socketRef.current.disconnect();
+ if (useCustomRange && !isSelectingRange) {
+ // Очищаем предыдущий таймер
+ if (debounceRef.current) {
+ clearTimeout(debounceRef.current);
}
- fetchCustomRangeData();
- return;
+
+ // Устанавливаем новый таймер с задержкой 500 мс
+ debounceRef.current = setTimeout(() => {
+ fetchCustomRangeData();
+ }, 500);
}
-
- if (!socketRef.current?.connected || isSelectingRange) return;
-
+
+ return () => {
+ if (debounceRef.current) {
+ clearTimeout(debounceRef.current);
+ }
+ };
+ }, [useCustomRange, isSelectingRange, startDate, endDate, fetchCustomRangeData]);
+
+ useEffect(() => {
+ if (useCustomRange || isSelectingRange) return;
+
const fetchDataWrapper = () => {
try {
fetchData();
@@ -291,46 +405,42 @@ const PrometheusChart = ({ metricName }) => {
console.error('Error in interval fetch:', error);
}
};
-
+
+ // Очищаем предыдущий интервал
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ }
+
+ // Запускаем сразу и затем по интервалу
fetchDataWrapper();
intervalRef.current = setInterval(fetchDataWrapper, selectedRange.interval);
-
+
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
- intervalRef.current = null;
}
};
- }, [fetchData, fetchCustomRangeData, selectedRange.interval, useCustomRange, isSelectingRange]);
+ }, [fetchData, selectedRange.interval, useCustomRange, isSelectingRange]);
useEffect(() => {
- if (!chartData || !selectedGraphRange) {
+ if (!selectedGraphRange || !chartData) {
setFilteredData(null);
return;
}
- const { startIndex, endIndex } = selectedGraphRange;
- const allTimes = Object.values(chartData)
- .flat()
- .map(point => point.time)
- .filter((time, index, self) => self.indexOf(time) === index);
+ const allPoints = Object.values(chartData).flat();
+ const sortedPoints = allPoints.sort((a, b) => a.timestamp - b.timestamp);
- const data = allTimes.map(time => {
- const point = { time };
- Object.keys(chartData).forEach(key => {
- const instanceData = chartData[key].find(p => p.time === time);
- point[key] = instanceData ? instanceData.value : null;
- });
- return point;
- }).sort((a, b) => {
- const timeA = chartData[Object.keys(chartData)[0]].find(d => d.time === a.time)?.timestamp;
- const timeB = chartData[Object.keys(chartData)[0]].find(d => d.time === b.time)?.timestamp;
- return timeA - timeB;
- });
+ const startIndex = Math.floor(selectedGraphRange.startIndex * (sortedPoints.length - 1));
+ const endIndex = Math.floor(selectedGraphRange.endIndex * (sortedPoints.length - 1));
- const filtered = data.slice(startIndex, endIndex + 1);
- setFilteredData(filtered);
- }, [selectedGraphRange, chartData]);
+ const filtered = sortedPoints.slice(startIndex, endIndex + 1);
+ const interpolated = filtered.length > 100 ?
+ interpolateData(filtered, 100) :
+ filtered;
+
+ setFilteredData(interpolated);
+ }, [selectedGraphRange, chartData, interpolateData]);
if (chartData === null) {
return Loading data...
;
@@ -373,7 +483,7 @@ const PrometheusChart = ({ metricName }) => {
chartData={chartData}
metricName={metricName}
colors={COLORS}
- onRangeSelect={handleRangeSelect} // Используем модифицированный обработчик
+ onRangeSelect={handleRangeSelect}
filteredData={filteredData}
/>
diff --git a/src/Components/TreeChart/tabContent.jsx b/src/Components/TreeChart/tabContent.jsx
index 58e6e49..c5353f5 100755
--- a/src/Components/TreeChart/tabContent.jsx
+++ b/src/Components/TreeChart/tabContent.jsx
@@ -7,8 +7,6 @@ const getMetricName = (id) => {
return `zvks_apiforsnmp_measure_${id}`;
};
-//!!!!!!!!!!Пофиксить вкладуи с eth4, во всех eth 1-4 открывается именно 4 !!!!!!!!!!!!!
-
// Функция для рекурсивного сбора всех id потомков
const getAllChildIds = (node) => {
let ids = [];
diff --git a/src/Components/hooks/TabContent.jsx b/src/Components/hooks/TabContent.jsx
index 10e58a0..bdb6226 100644
--- a/src/Components/hooks/TabContent.jsx
+++ b/src/Components/hooks/TabContent.jsx
@@ -1,6 +1,5 @@
import SystemStatusChart from "../../Charts/SystemStatusChart";
import TreeTable from "../UI/TreeTable";
-
import FlowChart from "../TreeChart/FlowChart";
const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleOpenTab }) => {