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

View File

@ -8,6 +8,7 @@ import useSidebarResize from "../hooks/useSidebarResize";
import TabContent from "../hooks/TabContent";
import menuData from "../TreeChart/menuData.json";
import SidebarMenuWrapper from "./SidebarMenuWrapper";
import MetricTabContent from "./MetricTabContent"
// Стилизованные компоненты
const DashboardContainer = styled(Box)(({ theme }) => ({
@ -82,12 +83,36 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => {
const handleMenuSelect = (item) => {
const tabId = `tab_${item.id}`;
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);
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 {
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);
};
// Статические вкладки
// Статические вкладки (сохраняем оригинальные id)
const staticTabs = [
{ id: "Главная", title: "Главная" },
{ id: "Визуализация", title: "Визуализация" }
@ -87,9 +87,9 @@ const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => {
{/* Статические вкладки */}
{staticTabs.map(tab => (
<StyledTab
key={tab.id}
key={`static_${tab.id}`} // Добавляем префикс для уникальности
label={tab.title}
value={tab.id}
value={tab.id} // Используем id как value
onMouseDown={(e) => handleMouseDown(e, tab.id)}
/>
))}
@ -97,7 +97,7 @@ const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => {
{/* Динамические вкладки */}
{tabs.map((tab) => (
<StyledTab
key={tab.id}
key={`dynamic_${tab.id}`} // Добавляем префикс для уникальности
label={
<TabLabel
title={tab.title}

View File

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