added ranges for charts
parent
12e1ff08f5
commit
b5b758ffa0
|
|
@ -8,19 +8,29 @@ import {
|
|||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
ReferenceArea
|
||||
ReferenceArea,
|
||||
ReferenceLine
|
||||
} from 'recharts';
|
||||
import { Tag } from 'antd';
|
||||
|
||||
// Цвета для граничных значений
|
||||
const rangeColors = {
|
||||
1: '#4CAF50', // зеленый (норма)
|
||||
2: '#FFC107', // желтый (предупреждение)
|
||||
3: '#FF9800', // оранжевый (опасно)
|
||||
4: '#F44336' // красный (критично)
|
||||
};
|
||||
|
||||
// ====== Вспомогательные функции ======
|
||||
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-серый
|
||||
}
|
||||
};
|
||||
switch (status) {
|
||||
case 0: return '#757575'; // серый (нет связи)
|
||||
case 1: return rangeColors[1]; // зеленый
|
||||
case 2: return rangeColors[2]; // желтый
|
||||
case 3: return rangeColors[3]; // оранжевый
|
||||
case 4: return rangeColors[4]; // красный
|
||||
default: return '#BDBDBD'; // серый по умолчанию
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status) => {
|
||||
return {
|
||||
|
|
@ -43,9 +53,9 @@ const getStatusDescription = (status) => {
|
|||
const StatusIndicator = ({ cx, cy, payload }) => {
|
||||
const status = payload?.status ?? 0;
|
||||
return (
|
||||
<circle
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
<circle
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
r={6}
|
||||
fill={getStatusColor(status)}
|
||||
stroke="#fff"
|
||||
|
|
@ -98,14 +108,15 @@ const CustomTooltip = ({ active, payload, label }) => {
|
|||
);
|
||||
};
|
||||
|
||||
// ====== Основной компонент ======
|
||||
const LineChartComponent = ({
|
||||
data,
|
||||
title,
|
||||
description,
|
||||
metaInfo,
|
||||
dataKey = 'value',
|
||||
height = 400
|
||||
height = 400,
|
||||
ranges = [],
|
||||
statusBoundaries = []
|
||||
}) => {
|
||||
|
||||
const getStatusAreas = () => {
|
||||
|
|
@ -137,6 +148,79 @@ const LineChartComponent = ({
|
|||
));
|
||||
};
|
||||
|
||||
const renderRangeAreas = () => {
|
||||
if (!ranges || ranges.length === 0) return null;
|
||||
|
||||
return ranges.map((range, index) => {
|
||||
const hasData = data && data.length > 0;
|
||||
const minX = hasData ? data[0].timestamp : 0;
|
||||
const maxX = hasData ? data[data.length - 1].timestamp : 0;
|
||||
|
||||
return (
|
||||
<ReferenceArea
|
||||
key={`range-${index}`}
|
||||
y1={range.min}
|
||||
y2={range.max}
|
||||
x1={minX}
|
||||
x2={maxX}
|
||||
fill={rangeColors[range.status] || '#e8e8e8'}
|
||||
fillOpacity={0.15}
|
||||
stroke={rangeColors[range.status] || '#e8e8e8'}
|
||||
strokeWidth={1}
|
||||
strokeDasharray="3 3"
|
||||
ifOverflow="extendDomain"
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const renderRangeLines = () => {
|
||||
if (!ranges || ranges.length === 0) return null;
|
||||
|
||||
const uniqueValues = new Set();
|
||||
ranges.forEach(range => {
|
||||
uniqueValues.add(range.min);
|
||||
uniqueValues.add(range.max);
|
||||
});
|
||||
|
||||
return Array.from(uniqueValues).map((value, index) => (
|
||||
<ReferenceLine
|
||||
key={`line-${index}`}
|
||||
y={value}
|
||||
stroke="#888"
|
||||
strokeDasharray="3 3"
|
||||
strokeOpacity={0.7}
|
||||
ifOverflow="extendDomain"
|
||||
label={{
|
||||
value: value.toFixed(1),
|
||||
position: 'insideRight',
|
||||
fill: '#888',
|
||||
fontSize: 12
|
||||
}}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
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
|
||||
}}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', height: `${height}px` }}>
|
||||
{title && <h3>{title}</h3>}
|
||||
|
|
@ -149,7 +233,52 @@ const LineChartComponent = ({
|
|||
</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
|
||||
data={data}
|
||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||
|
|
@ -160,6 +289,9 @@ const LineChartComponent = ({
|
|||
tickFormatter={(ts) => new Date(ts).toLocaleTimeString()}
|
||||
/>
|
||||
<YAxis />
|
||||
{renderRangeLines()}
|
||||
{renderRangeAreas()}
|
||||
{renderStatusBoundaries()} {/* Добавляем отображение границ */}
|
||||
{getStatusAreas()}
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Legend />
|
||||
|
|
@ -185,16 +317,16 @@ const LineChartComponent = ({
|
|||
flexWrap: 'wrap'
|
||||
}}>
|
||||
{[
|
||||
{ status: 1, label: '1 - Норма', color: '#4CAF50' },
|
||||
{ status: 2, label: '2 - Отклонение', color: '#FF9800' },
|
||||
{ status: 3, label: '3 - Критично', color: '#F44336' },
|
||||
{ status: 0, label: '0 - Нет связи', color: '#888' }
|
||||
{ status: 1, label: '1 - Норма' },
|
||||
{ status: 2, label: '2 - Отклонение' },
|
||||
{ status: 3, label: '3 - Критично' },
|
||||
{ status: 0, label: '0 - Нет связи' }
|
||||
].map(item => (
|
||||
<div key={item.status} style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
backgroundColor: item.color,
|
||||
backgroundColor: getStatusColor(item.status),
|
||||
marginRight: 8,
|
||||
borderRadius: '50%'
|
||||
}}></div>
|
||||
|
|
@ -206,4 +338,4 @@ const LineChartComponent = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default LineChartComponent;
|
||||
export default LineChartComponent;
|
||||
|
|
@ -8,13 +8,14 @@ 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 PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
|
||||
const {
|
||||
name: metricName,
|
||||
filters = {},
|
||||
title = metricName,
|
||||
description,
|
||||
context = {}
|
||||
context = {},
|
||||
ranges = []
|
||||
} = metricInfo || {};
|
||||
|
||||
const { device, source_id } = context;
|
||||
|
|
@ -39,18 +40,18 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => {
|
|||
|
||||
const formatMetricData = (dataArray) => {
|
||||
return dataArray
|
||||
.map(item => ({
|
||||
...item,
|
||||
timestamp: item.timestamp,
|
||||
value: parseFloat(item.value),
|
||||
name: item.__name__ || metricName,
|
||||
status: parseInt(item.status) || 0,
|
||||
device: item.device?.trim() || null,
|
||||
source_id: item.source_id || null,
|
||||
description: item.description || description
|
||||
}))
|
||||
.sort((a, b) => a.timestamp - b.timestamp);
|
||||
};
|
||||
.map(item => ({
|
||||
...item,
|
||||
timestamp: item.timestamp,
|
||||
value: parseFloat(item.value),
|
||||
name: item.__name__ || metricName,
|
||||
status: parseInt(item.status) || 0,
|
||||
device: item.device?.trim() || null,
|
||||
source_id: item.source_id || null,
|
||||
description: item.description || description
|
||||
}))
|
||||
.sort((a, b) => a.timestamp - b.timestamp);
|
||||
};
|
||||
|
||||
// Обновляем логи при изменении данных
|
||||
useEffect(() => {
|
||||
|
|
@ -64,7 +65,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => {
|
|||
setStatusLogs(newLogs);
|
||||
}
|
||||
}, [chartData]);
|
||||
|
||||
|
||||
|
||||
const fetchHistoricalData = async (start, end) => {
|
||||
setIsLoading(true);
|
||||
|
|
@ -239,6 +240,13 @@ const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => {
|
|||
device,
|
||||
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 && (
|
||||
<StatusLogTable logs={statusLogs} />
|
||||
|
|
|
|||
|
|
@ -84,7 +84,6 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => {
|
|||
const tabId = `tab_${item.id}`;
|
||||
const tabTitle = item.title || 'Новая вкладка';
|
||||
|
||||
// Если это метрика, создаём специальный контент с графиком
|
||||
const tabContent = item.metric
|
||||
? <MetricTabContent
|
||||
metricInfo={{
|
||||
|
|
@ -92,6 +91,7 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => {
|
|||
filters: item.filters,
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
ranges: item.ranges,
|
||||
context: {
|
||||
device: item.filters?.device,
|
||||
source_id: item.filters?.source_id,
|
||||
|
|
@ -110,13 +110,14 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => {
|
|||
content: tabContent,
|
||||
type: item.metric ? 'metric' : 'menuItem',
|
||||
metric: item.metric,
|
||||
filters: item.filters
|
||||
filters: item.filters,
|
||||
ranges: item.ranges
|
||||
};
|
||||
handleOpenTab(newTab);
|
||||
} else {
|
||||
setActiveTab(tabId);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Вспомогательная функция для получения всех дочерних элементов
|
||||
const getAllChildren = (node) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue