tabs and charts update

pull/40/head
DmitriyA 2025-05-27 22:14:48 -04:00
parent efd8532ac3
commit 08e2c24a63
6 changed files with 132 additions and 108 deletions

View File

@ -26,8 +26,11 @@ class MetricsService {
this.socket.on('connect', () => { this.socket.on('connect', () => {
console.log('WebSocket connected'); console.log('WebSocket connected');
this.subscriptions.forEach((_, metric) => { // Восстанавливаем подписки при переподключении
this.socket.emit('subscribe-metric', { metric }); this.subscriptions.forEach((_, metricKey) => {
const [metric, query] = metricKey.split('?');
const filters = this.parseFiltersFromKey(metricKey);
this.socket.emit('subscribe-metric', { metric, filters });
}); });
}); });
@ -83,35 +86,53 @@ class MetricsService {
}); });
} }
subscribeToMetric(metric, callback, interval = 5000, filters = {}) { subscribeToMetric(metricKey, callback, interval = 5000, filters = {}) {
this.connectWebSocket(); this.connectWebSocket();
const alreadySubscribed = this.subscriptions.has(metric); const alreadySubscribed = this.subscriptions.has(metricKey);
const callbacks = this.subscriptions.get(metric) || []; const callbacks = this.subscriptions.get(metricKey) || [];
callbacks.push(callback); callbacks.push(callback);
this.subscriptions.set(metric, callbacks); this.subscriptions.set(metricKey, callbacks);
if (!alreadySubscribed) { if (!alreadySubscribed) {
this.socket.emit('subscribe-metric', { metric, interval, filters }); // Разделяем metricKey на метрику и фильтры
const [metric] = metricKey.split('?');
this.socket.emit('subscribe-metric', {
metric,
interval,
filters
});
} }
return () => this.unsubscribeFromMetric(metric, callback); return () => this.unsubscribeFromMetric(metricKey, callback);
} }
unsubscribeFromMetric(metric, callback) { unsubscribeFromMetric(metricKey, callback) {
const callbacks = this.subscriptions.get(metric) || []; const callbacks = this.subscriptions.get(metricKey) || [];
const filtered = callbacks.filter(cb => cb !== callback); const filtered = callbacks.filter(cb => cb !== callback);
if (filtered.length === 0) { if (filtered.length === 0) {
this.subscriptions.delete(metric); this.subscriptions.delete(metricKey);
if (this.socket && this.socket.connected) { if (this.socket && this.socket.connected) {
const [metric] = metricKey.split('?');
this.socket.emit('unsubscribe-metric', { metric }); this.socket.emit('unsubscribe-metric', { metric });
} }
} else { } else {
this.subscriptions.set(metric, filtered); this.subscriptions.set(metricKey, filtered);
} }
} }
parseFiltersFromKey(metricKey) {
const parts = metricKey.split('?');
if (parts.length < 2) return {};
return parts[1].split('&').reduce((acc, pair) => {
const [key, value] = pair.split('=');
if (key && value) acc[key] = value;
return acc;
}, {});
}
cleanupAll() { cleanupAll() {
if (this.socket && this.socket.connected) { if (this.socket && this.socket.connected) {
this.socket.emit('unsubscribe-all'); this.socket.emit('unsubscribe-all');
@ -128,4 +149,7 @@ class MetricsService {
} }
} }
export const metricsService = new MetricsService(import.meta.env.VITE_BACK_URL); // Создаем экземпляр сервиса
const metricsService = new MetricsService(import.meta.env.VITE_BACK_URL);
export default metricsService;

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import LineChartComponent from './Components/LineChartComponent'; import LineChartComponent from './Components/LineChartComponent';
import DateRangeSelector from './Components/DateRangeSelector'; import DateRangeSelector from './Components/DateRangeSelector';
import { metricsService } from './Components/metricsService'; import metricsService from './Components/metricsService';
import { Button, Radio, message, Tag } from 'antd'; import { Button, Radio, message, Tag } from 'antd';
import moment from 'moment'; import moment from 'moment';
@ -11,14 +11,10 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => {
filters = {}, filters = {},
title = metricName, title = metricName,
description, description,
context = {} // Добавляем контекст из path context = {}
} = metricInfo || {}; } = metricInfo || {};
console.log("⚙️ PrometheusChart -> metricInfo:", metricInfo); const { device, source_id: module } = context;
console.log("📌 Контекст -> device:", context.device, "source_id:", context.source_id, "deviceId:", context.deviceId);
// Получаем полный контекст из родительских элементов
const { device, source_id: module, deviceId, parent } = context;
const [chartData, setChartData] = useState([]); const [chartData, setChartData] = useState([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -29,9 +25,11 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => {
const [endDate, setEndDate] = useState(moment().toDate()); const [endDate, setEndDate] = useState(moment().toDate());
const [isLiveUpdating, setIsLiveUpdating] = useState(false); const [isLiveUpdating, setIsLiveUpdating] = useState(false);
// Генерация уникального ключа для подписки
const getSubscriptionKey = () => { const getSubscriptionKey = () => {
return `${metricName}_${device || 'all'}_${module || 'all'}_${deviceId || 'all'}`; const filterParts = [];
if (device) filterParts.push(`device=${device}`);
if (module) filterParts.push(`source_id=${module}`);
return `${metricName}${filterParts.length ? `?${filterParts.join('&')}` : ''}`;
}; };
const formatMetricData = (dataArray) => { const formatMetricData = (dataArray) => {
@ -54,12 +52,10 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => {
try { try {
const extendedFilters = { const extendedFilters = {
...filters, ...filters,
...(device && { device: device.toString() }), // убедитесь, что device строка ...(device && { device: device.toString() }),
...(source_id && { source_id: source_id.toString() }) ...(module && { source_id: module.toString() })
}; };
console.log('Fetching with filters:', extendedFilters); // для отладки
const data = await metricsService.fetchMetricsRange( const data = await metricsService.fetchMetricsRange(
metricName, metricName,
Math.floor(start.getTime() / 1000), Math.floor(start.getTime() / 1000),
@ -97,24 +93,15 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => {
fetchHistoricalData(start, end).finally(() => setIsLoading(false)); fetchHistoricalData(start, end).finally(() => setIsLoading(false));
return metricsService.subscribeToMetric( return metricsService.subscribeToMetric(
getSubscriptionKey(), // Уникальный ключ для подписки getSubscriptionKey(),
(newData) => { (newData) => {
const filteredData = newData.filter(item => { const formattedData = formatMetricData(newData);
// Строгая проверка всех доступных фильтров
if (device && item.device?.trim() !== device) return false;
if (module && item.source_id !== module) return false;
return true;
});
if (filteredData.length > 0) {
const formattedData = formatMetricData(filteredData);
setChartData(prev => { setChartData(prev => {
const newChartData = [...prev, ...formattedData] const newChartData = [...prev, ...formattedData]
.filter((v, i, a) => a.findIndex(t => t.timestamp === v.timestamp) === i) .filter((v, i, a) => a.findIndex(t => t.timestamp === v.timestamp) === i)
.slice(-200); .slice(-200);
return newChartData; return newChartData;
}); });
}
}, },
5000, 5000,
{ {
@ -151,17 +138,6 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => {
}; };
}, [mode, metricName, device, module]); }, [mode, metricName, device, module]);
// Рекурсивно собираем путь для отображения
const getFullPath = () => {
const path = [];
let current = parent;
while (current) {
path.unshift(current.title);
current = current.parent;
}
return path.join(' > ');
};
const metaInfo = [ const metaInfo = [
metricMeta.instance && `Instance: ${metricMeta.instance}`, metricMeta.instance && `Instance: ${metricMeta.instance}`,
metricMeta.job && `Job: ${metricMeta.job}`, metricMeta.job && `Job: ${metricMeta.job}`,
@ -203,14 +179,8 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => {
)} )}
</div> </div>
{/* Отображаем полный путь к метрике */}
{parent && (
<div style={{ marginBottom: 16 }}>
<Tag color="blue">Путь: {getFullPath()}</Tag>
{device && <Tag color="geekblue">Устройство: {device}</Tag>} {device && <Tag color="geekblue">Устройство: {device}</Tag>}
{module && <Tag color="purple">Модуль: {module.split('$')[1]}</Tag>} {module && <Tag color="purple">Модуль: {module.split('$')[1]}</Tag>}
</div>
)}
{isLoading ? ( {isLoading ? (
<div>Загрузка графика...</div> <div>Загрузка графика...</div>

View File

@ -8,6 +8,7 @@ import useSidebarResize from "../hooks/useSidebarResize";
import TabContent from "../hooks/TabContent"; import TabContent from "../hooks/TabContent";
import menuData from "../TreeChart/menuData.json"; import menuData from "../TreeChart/menuData.json";
import SidebarMenuWrapper from "./SidebarMenuWrapper"; import SidebarMenuWrapper from "./SidebarMenuWrapper";
import MetricTabContent from "./MetricTabContent"
// Стилизованные компоненты // Стилизованные компоненты
const DashboardContainer = styled(Box)(({ theme }) => ({ const DashboardContainer = styled(Box)(({ theme }) => ({
@ -82,12 +83,36 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => {
const handleMenuSelect = (item) => { const handleMenuSelect = (item) => {
const tabId = `tab_${item.id}`; const tabId = `tab_${item.id}`;
const tabTitle = item.title || 'Новая вкладка'; const tabTitle = item.title || 'Новая вкладка';
const tabContent = <div style={{ padding: 20 }}>Контент для <strong>{item.title}</strong></div>;
// Если это метрика, создаём специальный контент с графиком
const tabContent = item.metric
? <MetricTabContent
metricInfo={{
name: item.metric,
filters: item.filters,
title: item.title,
description: item.description,
context: {
device: item.filters?.device,
source_id: item.filters?.source_id,
parent: item // для построения пути
}
}}
/>
: <div style={{ padding: 20 }}>Контент для <strong>{item.title}</strong></div>;
const existingTab = tabs.find(tab => tab.id === tabId); const existingTab = tabs.find(tab => tab.id === tabId);
if (!existingTab) { if (!existingTab) {
handleOpenTab(tabId, tabTitle, tabContent); // Передаем аргументы отдельно const newTab = {
id: tabId,
title: tabTitle,
content: tabContent,
type: item.metric ? 'metric' : 'menuItem',
metric: item.metric,
filters: item.filters
};
handleOpenTab(newTab);
} else { } else {
setActiveTab(tabId); setActiveTab(tabId);
} }

View File

@ -0,0 +1,25 @@
import React, { useEffect } from 'react';
import PrometheusChart from '../../Charts2/PrometheusChart';
import metricsService from '../../Charts2/Components/metricsService';
const MetricTabContent = ({ metricInfo }) => {
// Очистка подписок при закрытии вкладки
useEffect(() => {
return () => {
if (metricInfo?.name) {
metricsService.unsubscribeFromMetric(metricInfo.name);
}
};
}, [metricInfo?.name]);
return (
<div style={{ padding: 16 }}>
<PrometheusChart
metricInfo={metricInfo}
chartHeight={600}
/>
</div>
);
};
export default MetricTabContent;

View File

@ -62,7 +62,7 @@ const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => {
onTabClick(newValue); onTabClick(newValue);
}; };
// Статические вкладки // Статические вкладки (сохраняем оригинальные id)
const staticTabs = [ const staticTabs = [
{ id: "Главная", title: "Главная" }, { id: "Главная", title: "Главная" },
{ id: "Визуализация", title: "Визуализация" } { id: "Визуализация", title: "Визуализация" }
@ -87,9 +87,9 @@ const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => {
{/* Статические вкладки */} {/* Статические вкладки */}
{staticTabs.map(tab => ( {staticTabs.map(tab => (
<StyledTab <StyledTab
key={tab.id} key={`static_${tab.id}`} // Добавляем префикс для уникальности
label={tab.title} label={tab.title}
value={tab.id} value={tab.id} // Используем id как value
onMouseDown={(e) => handleMouseDown(e, tab.id)} onMouseDown={(e) => handleMouseDown(e, tab.id)}
/> />
))} ))}
@ -97,7 +97,7 @@ const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => {
{/* Динамические вкладки */} {/* Динамические вкладки */}
{tabs.map((tab) => ( {tabs.map((tab) => (
<StyledTab <StyledTab
key={tab.id} key={`dynamic_${tab.id}`} // Добавляем префикс для уникальности
label={ label={
<TabLabel <TabLabel
title={tab.title} title={tab.title}

View File

@ -4,43 +4,23 @@ const useTabs = (initialTab) => {
const [tabs, setTabs] = useState([]); const [tabs, setTabs] = useState([]);
const [activeTab, setActiveTab] = useState(initialTab); const [activeTab, setActiveTab] = useState(initialTab);
const handleOpenTab = useCallback((id, title, content) => { const handleOpenTab = useCallback((newTab) => {
setTabs((prevTabs) => { setTabs((prevTabs) => {
const existingTabIndex = prevTabs.findIndex(tab => tab.id === id); const exists = prevTabs.some((tab) => tab.id === newTab.id);
if (!exists) {
if (existingTabIndex >= 0) { return [...prevTabs, newTab];
return prevTabs.map((tab, index) => ({
...tab,
title: title || tab.title,
content: content || tab.content,
active: index === existingTabIndex
}));
} }
// Добавляем новую вкладку return prevTabs;
return [
...prevTabs.map(tab => ({ ...tab, active: false })),
{
id,
title,
content: content || <div>Loading...</div>,
active: true
}
];
}); });
setActiveTab(id); setActiveTab(newTab.id);
}, []); }, []);
const handleCloseTab = useCallback((id) => { const handleCloseTab = useCallback((id) => {
setTabs((prevTabs) => { setTabs((prevTabs) => {
const newTabs = prevTabs.filter((tab) => tab.id !== id); const newTabs = prevTabs.filter((tab) => tab.id !== id);
if (activeTab === id) { if (activeTab === id) {
setActiveTab(newTabs.length > 0 setActiveTab(newTabs.length > 0 ? newTabs[newTabs.length - 1].id : initialTab);
? newTabs[newTabs.length - 1].id
: initialTab
);
} }
return newTabs; return newTabs;
}); });
}, [activeTab, initialTab]); }, [activeTab, initialTab]);