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 }) => {