tabs and charts update
parent
efd8532ac3
commit
08e2c24a63
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue