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