Compare commits

..

No commits in common. "a931fd3ea4c75fbfc185a8ef069ff0b05384adff" and "350f3750150c8e7df7245d86f012e42ceae80fd9" have entirely different histories.

4 changed files with 78 additions and 253 deletions

View File

@ -8,27 +8,17 @@ import {
Tooltip, Tooltip,
Legend, Legend,
ResponsiveContainer, ResponsiveContainer,
ReferenceArea, ReferenceArea
ReferenceLine
} from 'recharts'; } from 'recharts';
import { Tag } from 'antd';
// Цвета для граничных значений
const rangeColors = {
1: '#4CAF50', // зеленый (норма)
2: '#FFC107', // желтый (предупреждение)
3: '#FF9800', // оранжевый (опасно)
4: '#F44336' // красный (критично)
};
// ====== Вспомогательные функции ======
const getStatusColor = (status) => { const getStatusColor = (status) => {
switch(status) { switch(status) {
case 0: return '#757575'; // серый (нет связи) case 0: return '#757575'; // тёмно-серый
case 1: return rangeColors[1]; // зеленый case 1: return '#00C853'; // ярко-зелёный
case 2: return rangeColors[2]; // желтый case 2: return '#FF6D00'; // ярко-оранжевый
case 3: return rangeColors[3]; // оранжевый case 3: return '#D50000'; // ярко-красный
case 4: return rangeColors[4]; // красный default: return '#BDBDBD'; // fallback-серый
default: return '#BDBDBD'; // серый по умолчанию
} }
}; };
@ -37,8 +27,7 @@ const getStatusText = (status) => {
0: 'Нет соединения', 0: 'Нет соединения',
1: 'Норма', 1: 'Норма',
2: 'Отклонение', 2: 'Отклонение',
3: 'Критично', 3: 'Критично'
4: 'Авария'
}[status] || 'Неизвестно'; }[status] || 'Неизвестно';
}; };
@ -47,8 +36,7 @@ const getStatusDescription = (status) => {
0: 'Устройство не отвечает', 0: 'Устройство не отвечает',
1: 'Параметры в норме', 1: 'Параметры в норме',
2: 'Обнаружены отклонения от нормы', 2: 'Обнаружены отклонения от нормы',
3: 'Критическое состояние системы', 3: 'Критическое состояние системы'
4: 'Авария'
}[status] || 'Статус неизвестен'; }[status] || 'Статус неизвестен';
}; };
@ -110,15 +98,14 @@ const CustomTooltip = ({ active, payload, label }) => {
); );
}; };
// ====== Основной компонент ======
const LineChartComponent = ({ const LineChartComponent = ({
data, data,
title, title,
description, description,
metaInfo, metaInfo,
dataKey = 'value', dataKey = 'value',
height = 400, height = 400
ranges = [],
statusBoundaries = []
}) => { }) => {
const getStatusAreas = () => { const getStatusAreas = () => {
@ -145,87 +132,7 @@ const LineChartComponent = ({
x1={area.start} x1={area.start}
x2={area.end} x2={area.end}
fill={getStatusColor(area.status)} fill={getStatusColor(area.status)}
fillOpacity={0.12} fillOpacity={0.1}
stroke={getStatusColor(area.status)}
strokeWidth={1}
strokeOpacity={0.5}
/>
));
};
const renderRangeLines = () => {
if (!ranges || ranges.length === 0) return null;
// Собираем только уникальные граничные значения, исключая дубликаты на стыках диапазонов
const boundaryValues = [];
ranges.forEach((range, index) => {
// Для первого диапазона добавляем и min и max
if (index === 0) {
boundaryValues.push(range.min);
boundaryValues.push(range.max);
}
// Для остальных добавляем только max (min будет совпадать с max предыдущего)
else {
boundaryValues.push(range.max);
}
});
return boundaryValues.map((value, index) => {
// Находим диапазон, к которому принадлежит эта граница
const range = ranges.find(r => r.min === value || r.max === value);
const status = range ? range.status : 1;
const lineStyle = {
1: { strokeWidth: 1, strokeDasharray: "none", opacity: 0.7 },
2: { strokeWidth: 2, strokeDasharray: "none", opacity: 0.9 },
3: { strokeWidth: 2, strokeDasharray: "none", opacity: 1 },
4: { strokeWidth: 2, strokeDasharray: "none", opacity: 1 }
}[status] || { strokeWidth: 1, strokeDasharray: "3 3", opacity: 0.7 };
return (
<ReferenceLine
key={`line-${value}`} // Используем значение как ключ для стабильности
y={value}
stroke={rangeColors[status] || '#888'}
strokeWidth={lineStyle.strokeWidth}
strokeDasharray={lineStyle.strokeDasharray}
strokeOpacity={lineStyle.opacity}
ifOverflow="extendDomain"
label={{
value: value.toFixed(1),
position: 'right',
fill: rangeColors[status] || '#888',
fontSize: 12,
fontWeight: 'bold',
background: 'rgba(255, 255, 255, 0.9)',
padding: [4, 6],
borderRadius: 4,
stroke: 'none',
boxShadow: '0 0 2px rgba(0,0,0,0.1)',
textAnchor: 'start'
}}
/>
);
});
};
const renderStatusBoundaries = () => {
if (!statusBoundaries || statusBoundaries.length === 0) return null;
return statusBoundaries.map((boundary, index) => (
<ReferenceLine
key={`boundary-${index}`}
x={boundary.timestamp}
stroke={getStatusColor(boundary.status)}
strokeWidth={2}
strokeDasharray="5 3"
label={{
value: boundary.label || `Граница ${index + 1}`,
position: 'top',
fill: getStatusColor(boundary.status),
fontSize: 12
}}
/> />
)); ));
}; };
@ -242,52 +149,7 @@ const LineChartComponent = ({
</div> </div>
)} )}
{/* Легенда граничных значений */} <ResponsiveContainer width="100%" height="80%">
{ranges.length > 0 && (
<div style={{ marginBottom: 10 }}>
<span style={{ marginRight: 8, fontWeight: 'bold' }}>Диапазоны:</span>
{ranges
.sort((a, b) => a.min - b.min)
.map((range, index) => (
<Tag
key={`range-tag-${index}`}
color={rangeColors[range.status] || 'default'}
style={{
marginRight: 5,
marginBottom: 5,
border: `1px solid ${rangeColors[range.status]}`,
background: `${rangeColors[range.status]}20`,
color: '#000000'
}}
>
{range.min.toFixed(0)}-{range.max.toFixed(0)} (Ур. {range.status})
</Tag>
))}
</div>
)}
{/* Легенда границ статусов */}
{statusBoundaries.length > 0 && (
<div style={{ marginBottom: 10 }}>
<span style={{ marginRight: 8, fontWeight: 'bold' }}>Границы статусов:</span>
{statusBoundaries.map((boundary, index) => (
<Tag
key={`boundary-tag-${index}`}
color={getStatusColor(boundary.status)}
style={{
marginRight: 5,
marginBottom: 5,
border: `1px solid ${getStatusColor(boundary.status)}`,
background: `${getStatusColor(boundary.status)}20`
}}
>
{boundary.label || `Граница ${index + 1}`} ({new Date(boundary.timestamp).toLocaleString()})
</Tag>
))}
</div>
)}
<ResponsiveContainer width="100%" height="75%">
<LineChart <LineChart
data={data} data={data}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
@ -298,8 +160,6 @@ const LineChartComponent = ({
tickFormatter={(ts) => new Date(ts).toLocaleTimeString()} tickFormatter={(ts) => new Date(ts).toLocaleTimeString()}
/> />
<YAxis /> <YAxis />
{renderRangeLines()}
{renderStatusBoundaries()}
{getStatusAreas()} {getStatusAreas()}
<Tooltip content={<CustomTooltip />} /> <Tooltip content={<CustomTooltip />} />
<Legend /> <Legend />
@ -314,8 +174,6 @@ const LineChartComponent = ({
name={title} name={title}
/> />
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
{/* Легенда статусов */} {/* Легенда статусов */}
@ -327,17 +185,16 @@ const LineChartComponent = ({
flexWrap: 'wrap' flexWrap: 'wrap'
}}> }}>
{[ {[
{ status: 1, label: '1 - Норма' }, { status: 1, label: '1 - Норма', color: '#4CAF50' },
{ status: 2, label: '2 - Отклонение' }, { status: 2, label: '2 - Отклонение', color: '#FF9800' },
{ status: 3, label: '3 - Критично' }, { status: 3, label: '3 - Критично', color: '#F44336' },
{ status: 4, label: '4 - Авария' }, { status: 0, label: '0 - Нет связи', color: '#888' }
{ status: 0, label: '0 - Нет связи' }
].map(item => ( ].map(item => (
<div key={item.status} style={{ display: 'flex', alignItems: 'center' }}> <div key={item.status} style={{ display: 'flex', alignItems: 'center' }}>
<div style={{ <div style={{
width: 16, width: 16,
height: 16, height: 16,
backgroundColor: getStatusColor(item.status), backgroundColor: item.color,
marginRight: 8, marginRight: 8,
borderRadius: '50%' borderRadius: '50%'
}}></div> }}></div>

View File

@ -1,3 +1,4 @@
// src/Components/StatusLogTable.jsx
import React from 'react'; import React from 'react';
import { import {
Table, Table,
@ -11,13 +12,11 @@ import {
Typography Typography
} from '@mui/material'; } from '@mui/material';
// Используем те же цвета, что и в LineChartComponent
const statusColors = { const statusColors = {
'0': '#757575', // серый (нет связи) '0': 'default',
'1': '#4CAF50', // зеленый (норма) '1': 'success',
'2': '#FFC107', // желтый (отклонение) '2': 'warning',
'3': '#FF9800', // оранжевый (критично) '3': 'error'
'4': '#F44336' // красный (авария)
}; };
const StatusLogTable = ({ logs }) => { const StatusLogTable = ({ logs }) => {
@ -45,12 +44,7 @@ const StatusLogTable = ({ logs }) => {
<TableCell> <TableCell>
<Chip <Chip
label={getStatusText(log.status)} label={getStatusText(log.status)}
style={{ color={statusColors[log.status] || 'default'}
backgroundColor: statusColors[log.status],
color: '#ffffff', // белый текст для лучшей читаемости
fontWeight: 'bold',
border: 'none'
}}
size="small" size="small"
/> />
</TableCell> </TableCell>
@ -68,14 +62,13 @@ const StatusLogTable = ({ logs }) => {
); );
}; };
// Вспомогательные функции (оставляем без изменений) // Вспомогательные функции
const getStatusText = (status) => { const getStatusText = (status) => {
const statusMap = { const statusMap = {
'0': 'Нет соединения', '0': 'Нет соединения',
'1': 'Норма', '1': 'Норма',
'2': 'Отклонение', '2': 'Отклонение',
'3': 'Критично', '3': 'Критично'
'4': 'Авария'
}; };
return statusMap[status] || 'Неизвестно'; return statusMap[status] || 'Неизвестно';
}; };
@ -85,8 +78,7 @@ const getStatusDescription = (status) => {
'0': 'Устройство не отвечает', '0': 'Устройство не отвечает',
'1': 'Параметры в норме', '1': 'Параметры в норме',
'2': 'Обнаружены отклонения от нормы', '2': 'Обнаружены отклонения от нормы',
'3': 'Критическое состояние системы', '3': 'Критическое состояние системы'
'4': 'Аварийное состояние системы'
}; };
return descriptions[status] || 'Статус неизвестен'; return descriptions[status] || 'Статус неизвестен';
}; };

View File

@ -8,14 +8,13 @@ import StatusLogTable from './Components/StatusLogTable';
import { Box, IconButton, Tooltip as MuiTooltip } from '@mui/material'; import { Box, IconButton, Tooltip as MuiTooltip } from '@mui/material';
import { ListAlt } from '@mui/icons-material'; import { ListAlt } from '@mui/icons-material';
const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => { const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => {
const { const {
name: metricName, name: metricName,
filters = {}, filters = {},
title = metricName, title = metricName,
description, description,
context = {}, context = {}
ranges = []
} = metricInfo || {}; } = metricInfo || {};
const { device, source_id } = context; const { device, source_id } = context;
@ -45,7 +44,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
timestamp: item.timestamp, timestamp: item.timestamp,
value: parseFloat(item.value), value: parseFloat(item.value),
name: item.__name__ || metricName, name: item.__name__ || metricName,
status: parseInt(item.status) || 0, status: item.status?.toString() || '0',
device: item.device?.trim() || null, device: item.device?.trim() || null,
source_id: item.source_id || null, source_id: item.source_id || null,
description: item.description || description description: item.description || description
@ -53,19 +52,6 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
.sort((a, b) => a.timestamp - b.timestamp); .sort((a, b) => a.timestamp - b.timestamp);
}; };
const downsampleData = (data, maxPoints = 500) => {
if (data.length <= maxPoints) return data;
const ratio = Math.ceil(data.length / maxPoints);
return data.filter((_, index) => index % ratio === 0);
};
const calculateStep = (startTime, endTime, maxPoints = 10000) => {
const seconds = (endTime.getTime() - startTime.getTime()) / 1000;
return Math.max(Math.ceil(seconds / maxPoints), 1); // в секундах
};
// Обновляем логи при изменении данных // Обновляем логи при изменении данных
useEffect(() => { useEffect(() => {
if (chartData.length > 0) { if (chartData.length > 0) {
@ -91,17 +77,15 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
...(source_id && { source_id: source_id.toString() }) ...(source_id && { source_id: source_id.toString() })
}; };
const step = calculateStep(start, end);
const data = await metricsService.fetchMetricsRange( const data = await metricsService.fetchMetricsRange(
metricName, metricName,
Math.floor(start.getTime() / 1000), Math.floor(start.getTime() / 1000),
Math.floor(end.getTime() / 1000), Math.floor(end.getTime() / 1000),
step, 15,
extendedFilters extendedFilters
); );
const formattedData = formatMetricData(data);
const formattedData = downsampleData(formatMetricData(data), 100); //КОЛИЧЕСТВО ТОЧЕК НА ГРАФИКЕ
if (formattedData.length > 0) { if (formattedData.length > 0) {
setMetricMeta({ setMetricMeta({
type: data[0]?.type, type: data[0]?.type,
@ -255,13 +239,6 @@ const data = await metricsService.fetchMetricsRange(
device, device,
source_id source_id
}} }}
ranges={ranges}
/*ranges={ranges.length > 0 ? ranges : [
{ min: 0, max: 60, status: 1 },
{ min: 60, max: 80, status: 2 },
{ min: 80, max: 90, status: 3 },
{ min: 90, max: 100, status: 4 }
]}*/
/> />
{showLogs && ( {showLogs && (
<StatusLogTable logs={statusLogs} /> <StatusLogTable logs={statusLogs} />

View File

@ -84,6 +84,7 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => {
const tabId = `tab_${item.id}`; const tabId = `tab_${item.id}`;
const tabTitle = item.title || 'Новая вкладка'; const tabTitle = item.title || 'Новая вкладка';
// Если это метрика, создаём специальный контент с графиком
const tabContent = item.metric const tabContent = item.metric
? <MetricTabContent ? <MetricTabContent
metricInfo={{ metricInfo={{
@ -91,7 +92,6 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => {
filters: item.filters, filters: item.filters,
title: item.title, title: item.title,
description: item.description, description: item.description,
ranges: item.ranges,
context: { context: {
device: item.filters?.device, device: item.filters?.device,
source_id: item.filters?.source_id, source_id: item.filters?.source_id,
@ -110,8 +110,7 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => {
content: tabContent, content: tabContent,
type: item.metric ? 'metric' : 'menuItem', type: item.metric ? 'metric' : 'menuItem',
metric: item.metric, metric: item.metric,
filters: item.filters, filters: item.filters
ranges: item.ranges
}; };
handleOpenTab(newTab); handleOpenTab(newTab);
} else { } else {