rc #45
|
|
@ -8,17 +8,27 @@ 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 '#00C853'; // ярко-зелёный
|
case 1: return rangeColors[1]; // зеленый
|
||||||
case 2: return '#FF6D00'; // ярко-оранжевый
|
case 2: return rangeColors[2]; // желтый
|
||||||
case 3: return '#D50000'; // ярко-красный
|
case 3: return rangeColors[3]; // оранжевый
|
||||||
default: return '#BDBDBD'; // fallback-серый
|
case 4: return rangeColors[4]; // красный
|
||||||
|
default: return '#BDBDBD'; // серый по умолчанию
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -27,7 +37,8 @@ const getStatusText = (status) => {
|
||||||
0: 'Нет соединения',
|
0: 'Нет соединения',
|
||||||
1: 'Норма',
|
1: 'Норма',
|
||||||
2: 'Отклонение',
|
2: 'Отклонение',
|
||||||
3: 'Критично'
|
3: 'Критично',
|
||||||
|
4: 'Авария'
|
||||||
}[status] || 'Неизвестно';
|
}[status] || 'Неизвестно';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -36,7 +47,8 @@ const getStatusDescription = (status) => {
|
||||||
0: 'Устройство не отвечает',
|
0: 'Устройство не отвечает',
|
||||||
1: 'Параметры в норме',
|
1: 'Параметры в норме',
|
||||||
2: 'Обнаружены отклонения от нормы',
|
2: 'Обнаружены отклонения от нормы',
|
||||||
3: 'Критическое состояние системы'
|
3: 'Критическое состояние системы',
|
||||||
|
4: 'Авария'
|
||||||
}[status] || 'Статус неизвестен';
|
}[status] || 'Статус неизвестен';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -98,14 +110,15 @@ 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 = () => {
|
||||||
|
|
@ -132,7 +145,87 @@ const LineChartComponent = ({
|
||||||
x1={area.start}
|
x1={area.start}
|
||||||
x2={area.end}
|
x2={area.end}
|
||||||
fill={getStatusColor(area.status)}
|
fill={getStatusColor(area.status)}
|
||||||
fillOpacity={0.1}
|
fillOpacity={0.12}
|
||||||
|
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
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
@ -149,7 +242,52 @@ 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 }}
|
||||||
|
|
@ -160,6 +298,8 @@ 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 />
|
||||||
|
|
@ -174,6 +314,8 @@ const LineChartComponent = ({
|
||||||
name={title}
|
name={title}
|
||||||
/>
|
/>
|
||||||
</LineChart>
|
</LineChart>
|
||||||
|
|
||||||
|
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
|
|
||||||
{/* Легенда статусов */}
|
{/* Легенда статусов */}
|
||||||
|
|
@ -185,16 +327,17 @@ const LineChartComponent = ({
|
||||||
flexWrap: 'wrap'
|
flexWrap: 'wrap'
|
||||||
}}>
|
}}>
|
||||||
{[
|
{[
|
||||||
{ status: 1, label: '1 - Норма', color: '#4CAF50' },
|
{ status: 1, label: '1 - Норма' },
|
||||||
{ status: 2, label: '2 - Отклонение', color: '#FF9800' },
|
{ status: 2, label: '2 - Отклонение' },
|
||||||
{ status: 3, label: '3 - Критично', color: '#F44336' },
|
{ status: 3, label: '3 - Критично' },
|
||||||
{ status: 0, label: '0 - Нет связи', color: '#888' }
|
{ status: 4, label: '4 - Авария' },
|
||||||
|
{ 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: item.color,
|
backgroundColor: getStatusColor(item.status),
|
||||||
marginRight: 8,
|
marginRight: 8,
|
||||||
borderRadius: '50%'
|
borderRadius: '50%'
|
||||||
}}></div>
|
}}></div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// src/Components/StatusLogTable.jsx
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
|
|
@ -12,11 +11,13 @@ import {
|
||||||
Typography
|
Typography
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
|
||||||
|
// Используем те же цвета, что и в LineChartComponent
|
||||||
const statusColors = {
|
const statusColors = {
|
||||||
'0': 'default',
|
'0': '#757575', // серый (нет связи)
|
||||||
'1': 'success',
|
'1': '#4CAF50', // зеленый (норма)
|
||||||
'2': 'warning',
|
'2': '#FFC107', // желтый (отклонение)
|
||||||
'3': 'error'
|
'3': '#FF9800', // оранжевый (критично)
|
||||||
|
'4': '#F44336' // красный (авария)
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatusLogTable = ({ logs }) => {
|
const StatusLogTable = ({ logs }) => {
|
||||||
|
|
@ -44,7 +45,12 @@ const StatusLogTable = ({ logs }) => {
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Chip
|
<Chip
|
||||||
label={getStatusText(log.status)}
|
label={getStatusText(log.status)}
|
||||||
color={statusColors[log.status] || 'default'}
|
style={{
|
||||||
|
backgroundColor: statusColors[log.status],
|
||||||
|
color: '#ffffff', // белый текст для лучшей читаемости
|
||||||
|
fontWeight: 'bold',
|
||||||
|
border: 'none'
|
||||||
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
@ -62,13 +68,14 @@ 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] || 'Неизвестно';
|
||||||
};
|
};
|
||||||
|
|
@ -78,7 +85,8 @@ const getStatusDescription = (status) => {
|
||||||
'0': 'Устройство не отвечает',
|
'0': 'Устройство не отвечает',
|
||||||
'1': 'Параметры в норме',
|
'1': 'Параметры в норме',
|
||||||
'2': 'Обнаружены отклонения от нормы',
|
'2': 'Обнаружены отклонения от нормы',
|
||||||
'3': 'Критическое состояние системы'
|
'3': 'Критическое состояние системы',
|
||||||
|
'4': 'Аварийное состояние системы'
|
||||||
};
|
};
|
||||||
return descriptions[status] || 'Статус неизвестен';
|
return descriptions[status] || 'Статус неизвестен';
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,14 @@ 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 = 560 }) => {
|
const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
|
||||||
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;
|
||||||
|
|
@ -44,7 +45,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => {
|
||||||
timestamp: item.timestamp,
|
timestamp: item.timestamp,
|
||||||
value: parseFloat(item.value),
|
value: parseFloat(item.value),
|
||||||
name: item.__name__ || metricName,
|
name: item.__name__ || metricName,
|
||||||
status: item.status?.toString() || '0',
|
status: parseInt(item.status) || 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
|
||||||
|
|
@ -52,6 +53,19 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => {
|
||||||
.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) {
|
||||||
|
|
@ -77,15 +91,17 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => {
|
||||||
...(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),
|
||||||
15,
|
step,
|
||||||
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,
|
||||||
|
|
@ -239,6 +255,13 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => {
|
||||||
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} />
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,6 @@ 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={{
|
||||||
|
|
@ -92,6 +91,7 @@ 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,7 +110,8 @@ 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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue