Compare commits

..

No commits in common. "fc1db66288191e6a5eee48fcb98e0db1ce9880ac" and "5ed1b448e5d679f0b6b8455445471356e039c560" have entirely different histories.

7 changed files with 128 additions and 269 deletions

View File

@ -1,15 +1,17 @@
import React, { useState } from 'react';
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer } from 'recharts';
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Line, ResponsiveContainer } from 'recharts';
const LineChartComponent = ({ chartData, metricName, colors, description, onRangeSelect, filteredData }) => {
const [selectionStart, setSelectionStart] = useState(null);
const [selectionEnd, setSelectionEnd] = useState(null);
// Создаем массив уникальных временных меток
const allTimes = Object.values(chartData)
.flat()
.map(point => point.time)
.filter((time, index, self) => self.indexOf(time) === index);
// Формируем данные для графика
const data = allTimes.map(time => {
const point = { time };
Object.keys(chartData).forEach(key => {
@ -19,8 +21,10 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
return point;
});
// Используем отфильтрованные данные, если они есть
const displayData = filteredData || data;
// Обработчик клика на графике
const handleClick = (e) => {
if (!e || !e.activeLabel) return;
@ -41,57 +45,40 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
}
};
// Упрощенный Tooltip без указания instance
// Кастомный Tooltip для отображения значения
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div className="custom-tooltip" style={{
backgroundColor: '#fff',
padding: '10px',
border: '1px solid #ccc',
borderRadius: '4px'
}}>
<p style={{ fontWeight: 'bold', marginBottom: '5px' }}>{`Время: ${label}`}</p>
<p>{`Значение: ${payload[0].value}`}</p>
<div className="custom-tooltip" style={{ padding: '10px' }}>
<p>{`Время: ${label}`}</p>
{payload.map((entry, index) => (
<p key={index} style={{}}>
{`Значение: ${entry.value}`}
</p>
))}
</div>
);
}
return null;
};
return (
<div>
<ResponsiveContainer width="100%" height={400}>
<LineChart
data={displayData}
onClick={handleClick}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis
dataKey="time"
tick={{ fill: '#666' }}
tickMargin={10}
/>
<YAxis
tick={{ fill: '#666' }}
tickMargin={10}
/>
<Tooltip
content={<CustomTooltip />}
cursor={{ stroke: '#ccc', strokeWidth: 1 }}
/>
{/* Убрали <Legend /> чтобы скрыть имена instance */}
<LineChart data={displayData} onClick={handleClick}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis />
<Tooltip content={<CustomTooltip />} />
<Legend />
{Object.keys(chartData).map((key, index) => (
<Line
key={key}
type="monotone"
dataKey={key}
stroke={colors[index % colors.length]}
strokeWidth={2}
dot={false}
activeDot={{ r: 6 }}
// Убрали name чтобы не отображалось в tooltip
name={key}
/>
))}
</LineChart>

View File

@ -125,7 +125,7 @@ const PrometheusChart = ({ metricName }) => {
? date.toLocaleString([], { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const key = m.instance;
const key = `${m.instance}-${m.device || m.scrape_job}`;
if (!updatedData[key]) updatedData[key] = {};
updatedData[key][formattedTime] = m.value;
});
@ -217,143 +217,41 @@ const PrometheusChart = ({ metricName }) => {
});
return (
<div style={{
backgroundColor: '#fff',
borderRadius: '8px',
padding: '20px',
marginBottom: '20px'
}}>
{/* Заголовок графика */}
<h3 style={{ marginTop: 0, color: '#333' }}>
</h3>
{/* Группа элементов управления */}
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '15px',
alignItems: 'center',
marginBottom: '15px'
}}>
{/* Стандартные диапазоны */}
<div style={{ flex: '1 1 200px' }}>
<label htmlFor="time-range" style={{
display: 'block',
marginBottom: '5px',
fontWeight: '500',
color: '#555'
}}>Стандартные диапазоны:</label>
<select
id="time-range"
value={selectedRange.value}
onChange={handleRangeChange}
style={{
width: '100%',
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #ddd',
color: "#333",
backgroundColor: '#f9f9f9'
}}
>
{TIME_RANGES.map(range => (
<option key={range.value} value={range.value}>{range.label}</option>
))}
</select>
</div>
{/* Кастомный диапазон */}
<div style={{ flex: '1 1 300px' }}>
<div style={{
marginBottom: '10px',
fontWeight: '500',
color: '#555'
}}>
Или укажите свой диапазон:
</div>
<div style={{
display: 'flex',
gap: '10px',
flexWrap: 'wrap'
}}>
<div style={{ flex: '1 1 200px' }}>
<DatePicker
selected={startDate}
onChange={(date) => setStartDate(date)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="yyyy-MM-dd HH:mm"
placeholderText="Начальная дата"
customInput={
<input style={{
backgroundColor: '#f9f9f9',
color: "#555",
width: '100%',
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #ddd'
}} />
}
/>
</div>
<div style={{ flex: '1 1 200px' }}>
<DatePicker
selected={endDate}
onChange={(date) => setEndDate(date)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="yyyy-MM-dd HH:mm"
placeholderText="Конечная дата"
customInput={
<input style={{
backgroundColor: '#f9f9f9',
color: "#555",
width: '100%',
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #ddd'
}} />
}
/>
</div>
<button
onClick={handleCustomRangeChange}
style={{
padding: '8px 16px',
backgroundColor: '#4a6baf',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
transition: 'background-color 0.2s',
flex: '0 0 auto',
alignSelf: 'flex-end'
}}
onMouseOver={(e) => e.target.style.backgroundColor = '#3a5a9f'}
onMouseOut={(e) => e.target.style.backgroundColor = '#4a6baf'}
>
Применить
</button>
</div>
</div>
<div>
<div>
<label htmlFor="time-range">Выберите временной диапазон: </label>
<select id="time-range" value={selectedRange.value} onChange={handleRangeChange}>
{TIME_RANGES.map(range => (
<option key={range.value} value={range.value}>{range.label}</option>
))}
</select>
</div>
{/* Индикатор текущего диапазона */}
<div style={{
margin: '10px 0',
padding: '8px 12px',
backgroundColor: '#f0f7ff',
borderRadius: '4px',
borderLeft: '3px solid #4a6baf'
}}>
Текущий диапазон: {useCustomRange
? `${startDate.toLocaleString()} - ${endDate.toLocaleString()}`
: selectedRange.label}
<div>
<label>Или выберите другой диапазон: </label>
<div>
<label>Начальная дата: </label>
<DatePicker
selected={startDate}
onChange={(date) => setStartDate(date)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="yyyy-MM-dd HH:mm"
/>
</div>
<div>
<label>Конечная дата: </label>
<DatePicker
selected={endDate}
onChange={(date) => setEndDate(date)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="yyyy-MM-dd HH:mm"
/>
</div>
<button onClick={handleCustomRangeChange}>Использовать кастомный диапазон</button>
</div>
{/* График */}
<LineChartComponent
chartData={chartData}
metricName={metricName}

View File

@ -8,7 +8,7 @@ import { useDataParser } from './FlowChartComponents/DataParser';
import NodeWrapper from './FlowChartComponents/NodeWrapper';
const nodeTypes = {
customNode: NodeWrapper
customNode: NodeWrapper // Должно совпадать с type в useDataParser
};
const FlowChart = ({ data }) => {
@ -40,17 +40,11 @@ const FlowChart = ({ data }) => {
setNodes(initialNodes);
setEdges(initialEdges);
// Автоматически сворачиваем узлы, которые являются родителями последнего уровня
if (!initialized.current && data) {
const findAndCollapseLastLevelParents = (items) => {
items.forEach(item => {
if (item.items && item.items.length > 0) {
// Проверяем, есть ли у детей свои дети
const hasGrandchildren = item.items.some(child =>
child.items && child.items.length > 0
);
// Если у детей нет своих детей - это родители последнего уровня
if (item.items?.length > 0) {
const hasGrandchildren = item.items.some(child => child.items?.length > 0);
if (!hasGrandchildren) {
toggleNodeCollapse(item.id);
} else {
@ -59,7 +53,6 @@ const FlowChart = ({ data }) => {
}
});
};
findAndCollapseLastLevelParents(data.items || []);
initialized.current = true;
}
@ -72,9 +65,7 @@ const FlowChart = ({ data }) => {
};
useEffect(() => {
return () => {
debouncedSetNodePositions.cancel();
};
return () => debouncedSetNodePositions.cancel();
}, [debouncedSetNodePositions]);
return (

View File

@ -1,34 +1,7 @@
import { useCallback } from 'react';
import { isLeafNode } from './nodeUtils';
import { getStatusColor } from '../dataUtils';
export const useDataParser = (nodePositions, collapsedNodes) => {
const getNodeStyle = useCallback((item, isLeaf) => ({
width: isLeaf ? 60 : 70,
height: isLeaf ? 60 : 70,
borderRadius: '50%',
backgroundColor: getStatusColor(item.status),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'black',
border: '2px solid #fff',
fontSize: isLeaf ? '0.8rem' : '1rem'
}), []);
const getCenterNodeStyle = useCallback((item) => ({
width: 80,
height: 80,
borderRadius: '50%',
backgroundColor: getStatusColor(item.status),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'black',
border: '2px solid #fff',
fontSize: '1.2rem'
}), []);
const parseData = useCallback((data) => {
if (!data) return { nodes: [], edges: [] };
@ -39,7 +12,7 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
const baseLevelRadius = 150;
const traverse = (item, parentId = null, level = 0, angleStart = 0, angleEnd = 2 * Math.PI, parentRadius = 0) => {
if (!item || collapsedNodes[parentId]) return; // Пропускаем свёрнутые узлы
if (!item || collapsedNodes[parentId]) return;
const nodeId = item.id;
const items = item.items || [];
@ -53,14 +26,15 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
const node = {
id: nodeId,
type: 'customNode',
position,
type: 'customNode', // Важно для кастомного рендеринга
data: {
...item,
label: item.title,
style: getNodeStyle(item, isLeaf), // Переносим стили в data
status: item.status,
hasChildren: items.length > 0,
collapsed: collapsedNodes[nodeId]
collapsed: collapsedNodes[nodeId],
isLeaf,
isCenterNode: parentId === null // Центральный узел
}
};
@ -71,14 +45,16 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
id: `${parentId}-${nodeId}`,
source: parentId,
target: nodeId,
style: { stroke: isLeaf ? '#aaa' : '#666', strokeWidth: isLeaf ? 1 : 2 }
style: {
stroke: isLeaf ? '#aaa' : '#666',
strokeWidth: isLeaf ? 1 : 2
}
});
}
if (!collapsedNodes[nodeId] && items.length > 0) {
const spreadAngle = angleEnd - angleStart;
items.forEach((child, index) => {
if (!child) return;
const itemAngleStart = angleStart + (index / items.length) * spreadAngle;
const itemAngleEnd = angleStart + ((index + 1) / items.length) * spreadAngle;
traverse(child, nodeId, level + 1, itemAngleStart, itemAngleEnd, parentRadius + baseLevelRadius);
@ -86,27 +62,33 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
}
};
// Центральный узел
const centerNode = {
id: data.id,
type: 'customNode', // Добавляем тип узла
position: nodePositions[data.id] || { x: centerX, y: centerY },
style: getCenterNodeStyle(data),
data: { label: data.title, hasChildren: data.items.length > 0, collapsed: collapsedNodes[data.id] }
type: 'customNode',
data: {
label: data.title,
status: data.status,
hasChildren: data.items.length > 0,
collapsed: collapsedNodes[data.id],
isLeaf: false,
isCenterNode: true
}
};
nodes.push(centerNode);
// Обработка дочерних узлов
if (!collapsedNodes[data.id] && data.items.length > 0) {
const angleStep = (2 * Math.PI) / data.items.length;
data.items.forEach((child, index) => {
if (!child) return;
traverse(child, data.id, 1, index * angleStep, (index + 1) * angleStep, 0);
});
}
return { nodes, edges };
}, [nodePositions, collapsedNodes, getNodeStyle, getCenterNodeStyle]);
}, [nodePositions, collapsedNodes]);
return { parseData };
};

View File

@ -1,63 +1,63 @@
import React, { memo } from 'react';
import { Handle } from 'reactflow';
import React from 'react';
import { getStatusColor } from '../dataUtils';
const NodeWrapper = ({ id, data, selected }) => {
// Параметры стиля
const size = data.isCenterNode ? 80 : (data.isLeaf ? 60 : 70);
const fontSize = data.isCenterNode ? '1.2rem' : (data.isLeaf ? '0.8rem' : '1rem');
const backgroundColor = getStatusColor(data.status);
// Базовый стиль узла
const nodeStyle = {
width: size,
height: size,
borderRadius: '50%',
backgroundColor,
border: `2px solid ${selected ? '#1890ff' : '#fff'}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize,
color: '#000',
position: 'relative',
boxShadow: selected ? '0 0 8px rgba(24, 144, 255, 0.5)' : 'none',
transition: 'all 0.2s ease'
};
const NodeWrapper = memo(({ id, data, selected }) => {
return (
<div
style={{
...data.style,
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden', // Чтобы текст не выходил за границы
textOverflow: 'ellipsis', // Добавляем многоточие если текст не помещается
whiteSpace: 'nowrap', // Запрещаем перенос строк
padding: '0 8px', // Горизонтальный padding для текста
boxSizing: 'border-box' // Учитываем padding в общей ширине
}}
title={data.label} // Простой tooltip при наведении
>
{/* Хендл для входящих соединений */}
<Handle
type="target"
position="top"
style={{ visibility: 'hidden' }}
/>
{/* Обёртка для текста с ограничением ширины */}
<div style={nodeStyle}>
<div style={{
maxWidth: '100%',
padding: '0 8px',
overflow: 'hidden',
textOverflow: 'ellipsis'
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}>
{data.label}
</div>
{data.hasChildren && (
<span style={{
<div style={{
position: 'absolute',
top: 5,
right: 5,
fontSize: '12px',
cursor: 'pointer',
top: -8,
right: -8,
width: 20,
height: 20,
borderRadius: '50%',
background: '#fff',
padding: '2px 5px',
borderRadius: '3px',
border: '1px solid #aaa'
border: '1px solid #ddd',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 10,
fontWeight: 'bold',
cursor: 'pointer',
zIndex: 10
}}>
{data.collapsed ? '+' : '-'}
</span>
</div>
)}
{/* Хендл для исходящих соединений */}
<Handle
type="source"
position="bottom"
style={{ visibility: 'hidden' }}
/>
</div>
);
});
};
export default NodeWrapper;
export default React.memo(NodeWrapper);

View File

@ -17,6 +17,7 @@ const LoginModal = ({ onLogin, onClose }) => {
try {
// Отправляем данные на бэкенд
console.log("Отправляем данные:", { username, password });
const response = await fetch('http://192.168.2.39:3000/auth/login', {
method: 'POST',
headers: {

View File