redesign of graphs and visualizations
parent
5ed1b448e5
commit
c077449b2c
|
|
@ -217,41 +217,143 @@ const PrometheusChart = ({ metricName }) => {
|
|||
});
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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 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>
|
||||
<label>Конечная дата: </label>
|
||||
<DatePicker
|
||||
selected={endDate}
|
||||
onChange={(date) => setEndDate(date)}
|
||||
showTimeSelect
|
||||
timeFormat="HH:mm"
|
||||
timeIntervals={15}
|
||||
dateFormat="yyyy-MM-dd HH:mm"
|
||||
/>
|
||||
|
||||
{/* Кастомный диапазон */}
|
||||
<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>
|
||||
<button onClick={handleCustomRangeChange}>Использовать кастомный диапазон</button>
|
||||
</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>
|
||||
|
||||
{/* График */}
|
||||
<LineChartComponent
|
||||
chartData={chartData}
|
||||
metricName={metricName}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { useDataParser } from './FlowChartComponents/DataParser';
|
|||
import NodeWrapper from './FlowChartComponents/NodeWrapper';
|
||||
|
||||
const nodeTypes = {
|
||||
customNode: NodeWrapper // Должно совпадать с type в useDataParser
|
||||
customNode: NodeWrapper
|
||||
};
|
||||
|
||||
const FlowChart = ({ data }) => {
|
||||
|
|
@ -40,11 +40,17 @@ const FlowChart = ({ data }) => {
|
|||
setNodes(initialNodes);
|
||||
setEdges(initialEdges);
|
||||
|
||||
// Автоматически сворачиваем узлы, которые являются родителями последнего уровня
|
||||
if (!initialized.current && data) {
|
||||
const findAndCollapseLastLevelParents = (items) => {
|
||||
items.forEach(item => {
|
||||
if (item.items?.length > 0) {
|
||||
const hasGrandchildren = item.items.some(child => child.items?.length > 0);
|
||||
if (item.items && item.items.length > 0) {
|
||||
// Проверяем, есть ли у детей свои дети
|
||||
const hasGrandchildren = item.items.some(child =>
|
||||
child.items && child.items.length > 0
|
||||
);
|
||||
|
||||
// Если у детей нет своих детей - это родители последнего уровня
|
||||
if (!hasGrandchildren) {
|
||||
toggleNodeCollapse(item.id);
|
||||
} else {
|
||||
|
|
@ -53,6 +59,7 @@ const FlowChart = ({ data }) => {
|
|||
}
|
||||
});
|
||||
};
|
||||
|
||||
findAndCollapseLastLevelParents(data.items || []);
|
||||
initialized.current = true;
|
||||
}
|
||||
|
|
@ -65,7 +72,9 @@ const FlowChart = ({ data }) => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => debouncedSetNodePositions.cancel();
|
||||
return () => {
|
||||
debouncedSetNodePositions.cancel();
|
||||
};
|
||||
}, [debouncedSetNodePositions]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,34 @@
|
|||
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: [] };
|
||||
|
||||
|
|
@ -12,7 +39,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 || [];
|
||||
|
|
@ -26,15 +53,14 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
|
|||
|
||||
const node = {
|
||||
id: nodeId,
|
||||
type: 'customNode',
|
||||
position,
|
||||
type: 'customNode', // Важно для кастомного рендеринга
|
||||
data: {
|
||||
...item,
|
||||
label: item.title,
|
||||
status: item.status,
|
||||
style: getNodeStyle(item, isLeaf), // Переносим стили в data
|
||||
hasChildren: items.length > 0,
|
||||
collapsed: collapsedNodes[nodeId],
|
||||
isLeaf,
|
||||
isCenterNode: parentId === null // Центральный узел
|
||||
collapsed: collapsedNodes[nodeId]
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -45,16 +71,14 @@ 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);
|
||||
|
|
@ -62,33 +86,27 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
|
|||
}
|
||||
};
|
||||
|
||||
// Центральный узел
|
||||
const centerNode = {
|
||||
id: data.id,
|
||||
type: 'customNode', // Добавляем тип узла
|
||||
position: nodePositions[data.id] || { x: centerX, y: centerY },
|
||||
type: 'customNode',
|
||||
data: {
|
||||
label: data.title,
|
||||
status: data.status,
|
||||
hasChildren: data.items.length > 0,
|
||||
collapsed: collapsedNodes[data.id],
|
||||
isLeaf: false,
|
||||
isCenterNode: true
|
||||
}
|
||||
style: getCenterNodeStyle(data),
|
||||
data: { label: data.title, hasChildren: data.items.length > 0, collapsed: collapsedNodes[data.id] }
|
||||
};
|
||||
|
||||
|
||||
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]);
|
||||
}, [nodePositions, collapsedNodes, getNodeStyle, getCenterNodeStyle]);
|
||||
|
||||
return { parseData };
|
||||
};
|
||||
|
|
@ -1,63 +1,63 @@
|
|||
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'
|
||||
};
|
||||
import React, { memo } from 'react';
|
||||
import { Handle } from 'reactflow';
|
||||
|
||||
const NodeWrapper = memo(({ id, data, selected }) => {
|
||||
return (
|
||||
<div style={nodeStyle}>
|
||||
<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={{
|
||||
padding: '0 8px',
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
textOverflow: 'ellipsis'
|
||||
}}>
|
||||
{data.label}
|
||||
</div>
|
||||
|
||||
{data.hasChildren && (
|
||||
<div style={{
|
||||
<span style={{
|
||||
position: 'absolute',
|
||||
top: -8,
|
||||
right: -8,
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: '50%',
|
||||
background: '#fff',
|
||||
border: '1px solid #ddd',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 10,
|
||||
fontWeight: 'bold',
|
||||
top: 5,
|
||||
right: 5,
|
||||
fontSize: '12px',
|
||||
cursor: 'pointer',
|
||||
zIndex: 10
|
||||
background: '#fff',
|
||||
padding: '2px 5px',
|
||||
borderRadius: '3px',
|
||||
border: '1px solid #aaa'
|
||||
}}>
|
||||
{data.collapsed ? '+' : '-'}
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Хендл для исходящих соединений */}
|
||||
<Handle
|
||||
type="source"
|
||||
position="bottom"
|
||||
style={{ visibility: 'hidden' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default React.memo(NodeWrapper);
|
||||
export default NodeWrapper;
|
||||
|
|
@ -17,7 +17,6 @@ const LoginModal = ({ onLogin, onClose }) => {
|
|||
|
||||
try {
|
||||
// Отправляем данные на бэкенд
|
||||
console.log("Отправляем данные:", { username, password });
|
||||
const response = await fetch('http://192.168.2.39:3000/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
.range-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
background-color: #f9f9f9;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.range-selector label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.range-selector select,
|
||||
.range-selector button {
|
||||
padding: 8px;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 5px;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-range {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.custom-range div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.apply-button {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.apply-button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
Loading…
Reference in New Issue