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 => (
+
+ ))}
+
+
+ );
+};
+
+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
}
}}
/>