diff --git a/src/Charts2/Components/LineChartComponent.jsx b/src/Charts2/Components/LineChartComponent.jsx index 2f990f6..d7df0ef 100644 --- a/src/Charts2/Components/LineChartComponent.jsx +++ b/src/Charts2/Components/LineChartComponent.jsx @@ -1,68 +1,209 @@ import React from 'react'; -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, + ReferenceArea +} from 'recharts'; -const LineChartComponent = ({ - data, - title, - description, - metaInfo, - dataKey = 'value', - lineColor = '#8884d8', - height = 400, - showLegend = true, - showGrid = true, - customTooltip, - customXAxisFormatter, - customYAxis, - additionalLines = [] -}) => { - return ( -
- {title &&

{title}

} - {description && ( -

{description}

- )} - {metaInfo && ( -
- {metaInfo} -
- )} +// ====== Вспомогательные функции ====== +const getStatusColor = (status) => { + switch(status) { + case 0: return '#757575'; // тёмно-серый + case 1: return '#00C853'; // ярко-зелёный + case 2: return '#FF6D00'; // ярко-оранжевый + case 3: return '#D50000'; // ярко-красный + default: return '#BDBDBD'; // fallback-серый + } + }; - - - {showGrid && } - new Date(timestamp).toLocaleTimeString())} - /> - {customYAxis || } - new Date(timestamp).toLocaleString()} - /> - {showLegend && } - - {additionalLines.map((lineProps, index) => ( - - ))} - - -
- ); +const getStatusText = (status) => { + return { + 0: 'Нет соединения', + 1: 'Норма', + 2: 'Отклонение', + 3: 'Критично' + }[status] || 'Неизвестно'; }; -export default LineChartComponent; \ No newline at end of file +const getStatusDescription = (status) => { + return { + 0: 'Устройство не отвечает', + 1: 'Параметры в норме', + 2: 'Обнаружены отклонения от нормы', + 3: 'Критическое состояние системы' + }[status] || 'Статус неизвестен'; +}; + +const StatusIndicator = ({ cx, cy, payload }) => { + const status = payload?.status ?? 0; + return ( + + ); +}; + +const CustomTooltip = ({ active, payload, label }) => { + if (!active || !payload || !payload.length) return null; + + const status = payload[0].payload.status; + const statusColor = getStatusColor(status); + + return ( +
+

{new Date(label).toLocaleString()}

+

+ Значение: {payload[0].value.toFixed(2)} +

+
+ +
+ {getStatusText(status)} +
+ {getStatusDescription(status)} +
+
+
+
+ ); +}; + +// ====== Основной компонент ====== +const LineChartComponent = ({ + data, + title, + description, + metaInfo, + dataKey = 'value', + height = 400 +}) => { + + const getStatusAreas = () => { + if (!data || data.length === 0) return null; + + const areas = []; + let currentStatus = data[0].status; + let start = data[0].timestamp; + + for (let i = 1; i < data.length; i++) { + const current = data[i]; + if (current.status !== currentStatus) { + areas.push({ status: currentStatus, start, end: current.timestamp }); + currentStatus = current.status; + start = current.timestamp; + } + } + + areas.push({ status: currentStatus, start, end: data[data.length - 1].timestamp }); + + return areas.map((area, i) => ( + + )); + }; + + return ( +
+ {title &&

{title}

} + {description && ( +

{description}

+ )} + {metaInfo && ( +
+ {metaInfo} +
+ )} + + + + + new Date(ts).toLocaleTimeString()} + /> + + {getStatusAreas()} + } /> + + } + activeDot={{ r: 8 }} + isAnimationActive={false} + name={title} + /> + + + + {/* Легенда статусов */} +
+ {[ + { status: 1, label: '1 - Норма', color: '#4CAF50' }, + { status: 2, label: '2 - Отклонение', color: '#FF9800' }, + { status: 3, label: '3 - Критично', color: '#F44336' }, + { status: 0, label: '0 - Нет связи', color: '#888' } + ].map(item => ( +
+
+ {item.label} +
+ ))} +
+
+ ); +}; + +export default LineChartComponent; diff --git a/src/Charts2/Components/StatusLogTable.jsx b/src/Charts2/Components/StatusLogTable.jsx new file mode 100644 index 0000000..33dbc06 --- /dev/null +++ b/src/Charts2/Components/StatusLogTable.jsx @@ -0,0 +1,86 @@ +// src/Components/StatusLogTable.jsx +import React from 'react'; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Chip, + Typography +} from '@mui/material'; + +const statusColors = { + '0': 'default', + '1': 'success', + '2': 'warning', + '3': 'error' +}; + +const StatusLogTable = ({ logs }) => { + return ( + + + + + Время + Устройство + Модуль + Статус + Значение + Описание + + + + {logs.map((log, index) => ( + + + {new Date(log.timestamp).toLocaleString()} + + {log.device} + {log.source_id?.split('$')[1]} + + + + {parseFloat(log.value).toFixed(2)} + + + {log.description || getStatusDescription(log.status)} + + + + ))} + +
+
+ ); +}; + +// Вспомогательные функции +const getStatusText = (status) => { + const statusMap = { + '0': 'Нет соединения', + '1': 'Норма', + '2': 'Отклонение', + '3': 'Критично' + }; + return statusMap[status] || 'Неизвестно'; +}; + +const getStatusDescription = (status) => { + const descriptions = { + '0': 'Устройство не отвечает', + '1': 'Параметры в норме', + '2': 'Обнаружены отклонения от нормы', + '3': 'Критическое состояние системы' + }; + return descriptions[status] || 'Статус неизвестен'; +}; + +export default StatusLogTable; \ No newline at end of file diff --git a/src/Charts2/Components/metricsService.jsx b/src/Charts2/Components/metricsService.jsx index 90d9261..0550ba0 100644 --- a/src/Charts2/Components/metricsService.jsx +++ b/src/Charts2/Components/metricsService.jsx @@ -6,6 +6,8 @@ class MetricsService { this.socket = null; this.subscriptions = new Map(); this.pendingRequests = new Map(); + window.addEventListener('beforeunload', this.cleanupAll.bind(this)); + window.addEventListener('pagehide', this.cleanupAll.bind(this)); window.addEventListener('beforeunload', () => { this.cleanupAll(); @@ -40,6 +42,7 @@ class MetricsService { }); this.socket.on('metrics-data', ({ metric, data, requestId }) => { + console.log('Incoming metric update:', metric); if (requestId && this.pendingRequests.has(requestId)) { const { resolve } = this.pendingRequests.get(requestId); resolve(data); @@ -88,14 +91,9 @@ class MetricsService { subscribeToMetric(metricKey, callback, interval = 5000, filters = {}) { this.connectWebSocket(); - - const alreadySubscribed = this.subscriptions.has(metricKey); - const callbacks = this.subscriptions.get(metricKey) || []; - callbacks.push(callback); - this.subscriptions.set(metricKey, callbacks); - - if (!alreadySubscribed) { - // Разделяем metricKey на метрику и фильтры + + if (!this.subscriptions.has(metricKey)) { + this.subscriptions.set(metricKey, []); const [metric] = metricKey.split('?'); this.socket.emit('subscribe-metric', { metric, @@ -103,8 +101,13 @@ class MetricsService { filters }); } - - return () => this.unsubscribeFromMetric(metricKey, callback); + + const callbacks = this.subscriptions.get(metricKey); + callbacks.push(callback); + + return () => { + this.unsubscribeFromMetric(metricKey, callback); + }; } unsubscribeFromMetric(metricKey, callback) { diff --git a/src/Charts2/PrometheusChart.jsx b/src/Charts2/PrometheusChart.jsx index 5d7fe46..a0be805 100644 --- a/src/Charts2/PrometheusChart.jsx +++ b/src/Charts2/PrometheusChart.jsx @@ -4,6 +4,9 @@ import DateRangeSelector from './Components/DateRangeSelector'; import metricsService from './Components/metricsService'; import { Button, Radio, message, Tag } from 'antd'; import moment from 'moment'; +import StatusLogTable from './Components/StatusLogTable'; +import { Box, IconButton, Tooltip as MuiTooltip } from '@mui/material'; +import { ListAlt } from '@mui/icons-material'; const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => { const { @@ -14,7 +17,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => { context = {} } = metricInfo || {}; - const { device, source_id: module } = context; + const { device, source_id } = context; const [chartData, setChartData] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -24,27 +27,45 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => { const [startDate, setStartDate] = useState(moment().subtract(1, 'hour').toDate()); const [endDate, setEndDate] = useState(moment().toDate()); const [isLiveUpdating, setIsLiveUpdating] = useState(false); + const [showLogs, setShowLogs] = useState(false); + const [statusLogs, setStatusLogs] = useState([]); const getSubscriptionKey = () => { const filterParts = []; - if (device) filterParts.push(`device=${device}`); - if (module) filterParts.push(`source_id=${module}`); + if (device) filterParts.push(`device=${encodeURIComponent(device)}`); + if (source_id) filterParts.push(`source_id=${encodeURIComponent(source_id)}`); return `${metricName}${filterParts.length ? `?${filterParts.join('&')}` : ''}`; }; const formatMetricData = (dataArray) => { return dataArray .map(item => ({ + ...item, timestamp: item.timestamp, value: parseFloat(item.value), name: item.__name__ || metricName, - status: item.status, + status: item.status?.toString() || '0', device: item.device?.trim() || null, - source_id: item.source_id || null + source_id: item.source_id || null, + description: item.description || description })) .sort((a, b) => a.timestamp - b.timestamp); }; + // Обновляем логи при изменении данных + useEffect(() => { + if (chartData.length > 0) { + const newLogs = chartData.reduce((acc, point, index) => { + if (index === 0 || point.status !== chartData[index - 1].status) { + return [...acc, point]; + } + return acc; + }, []); + setStatusLogs(newLogs); + } + }, [chartData]); + + const fetchHistoricalData = async (start, end) => { setIsLoading(true); setError(null); @@ -53,7 +74,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => { const extendedFilters = { ...filters, ...(device && { device: device.toString() }), - ...(module && { source_id: module.toString() }) + ...(source_id && { source_id: source_id.toString() }) }; const data = await metricsService.fetchMetricsRange( @@ -107,7 +128,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => { { ...filters, ...(device && { device }), - ...(module && { source_id: module }) + ...(source_id && { source_id }) } ); }; @@ -124,6 +145,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => { }; useEffect(() => { + console.log('Current metric context:', { device, source_id, metricName }); let unsubscribe; if (mode === 'realtime') { unsubscribe = startRealtimeUpdates(); @@ -136,7 +158,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => { if (unsubscribe) unsubscribe(); stopRealtimeUpdates(); }; - }, [mode, metricName, device, module]); + }, [mode, metricName, device, source_id]); const metaInfo = [ metricMeta.instance && `Instance: ${metricMeta.instance}`, @@ -180,27 +202,50 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => { {device && Устройство: {device}} - {module && Модуль: {module.split('$')[1]}} + {source_id && Модуль: {source_id.split('$')[1]}} - {isLoading ? ( -
Загрузка графика...
- ) : error ? ( -
Ошибка: {error}
- ) : chartData.length === 0 ? ( -
Нет данных для метрики: {metricName}
- ) : ( - - )} + + + setShowLogs(!showLogs)} + sx={{ + position: 'absolute', + right: 16, + top: 16, + zIndex: 1000, + bgcolor: 'background.paper', + boxShadow: 1 + }} + > + + + + + {isLoading ? ( +
Загрузка графика...
+ ) : error ? ( +
Ошибка: {error}
+ ) : chartData.length === 0 ? ( +
Нет данных для метрики: {metricName}
+ ) : ( + <> + + {showLogs && ( + + )} + + )} +
); }; diff --git a/src/Components/Layout/Dashboard.jsx b/src/Components/Layout/Dashboard.jsx index 8841bc7..b5b3768 100755 --- a/src/Components/Layout/Dashboard.jsx +++ b/src/Components/Layout/Dashboard.jsx @@ -95,7 +95,7 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => { context: { device: item.filters?.device, source_id: item.filters?.source_id, - parent: item // для построения пути + parent: item } }} />