Compare commits
No commits in common. "fc1db66288191e6a5eee48fcb98e0db1ce9880ac" and "5ed1b448e5d679f0b6b8455445471356e039c560" have entirely different histories.
fc1db66288
...
5ed1b448e5
|
|
@ -1,15 +1,17 @@
|
||||||
import React, { useState } from 'react';
|
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 LineChartComponent = ({ chartData, metricName, colors, description, onRangeSelect, filteredData }) => {
|
||||||
const [selectionStart, setSelectionStart] = useState(null);
|
const [selectionStart, setSelectionStart] = useState(null);
|
||||||
const [selectionEnd, setSelectionEnd] = useState(null);
|
const [selectionEnd, setSelectionEnd] = useState(null);
|
||||||
|
|
||||||
|
// Создаем массив уникальных временных меток
|
||||||
const allTimes = Object.values(chartData)
|
const allTimes = Object.values(chartData)
|
||||||
.flat()
|
.flat()
|
||||||
.map(point => point.time)
|
.map(point => point.time)
|
||||||
.filter((time, index, self) => self.indexOf(time) === index);
|
.filter((time, index, self) => self.indexOf(time) === index);
|
||||||
|
|
||||||
|
// Формируем данные для графика
|
||||||
const data = allTimes.map(time => {
|
const data = allTimes.map(time => {
|
||||||
const point = { time };
|
const point = { time };
|
||||||
Object.keys(chartData).forEach(key => {
|
Object.keys(chartData).forEach(key => {
|
||||||
|
|
@ -19,8 +21,10 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
|
||||||
return point;
|
return point;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Используем отфильтрованные данные, если они есть
|
||||||
const displayData = filteredData || data;
|
const displayData = filteredData || data;
|
||||||
|
|
||||||
|
// Обработчик клика на графике
|
||||||
const handleClick = (e) => {
|
const handleClick = (e) => {
|
||||||
if (!e || !e.activeLabel) return;
|
if (!e || !e.activeLabel) return;
|
||||||
|
|
||||||
|
|
@ -41,57 +45,40 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Упрощенный Tooltip без указания instance
|
// Кастомный Tooltip для отображения значения
|
||||||
const CustomTooltip = ({ active, payload, label }) => {
|
const CustomTooltip = ({ active, payload, label }) => {
|
||||||
if (active && payload && payload.length) {
|
if (active && payload && payload.length) {
|
||||||
return (
|
return (
|
||||||
<div className="custom-tooltip" style={{
|
<div className="custom-tooltip" style={{ padding: '10px' }}>
|
||||||
backgroundColor: '#fff',
|
<p>{`Время: ${label}`}</p>
|
||||||
padding: '10px',
|
{payload.map((entry, index) => (
|
||||||
border: '1px solid #ccc',
|
<p key={index} style={{}}>
|
||||||
borderRadius: '4px'
|
{`Значение: ${entry.value}`}
|
||||||
}}>
|
</p>
|
||||||
<p style={{ fontWeight: 'bold', marginBottom: '5px' }}>{`Время: ${label}`}</p>
|
))}
|
||||||
<p>{`Значение: ${payload[0].value}`}</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
<ResponsiveContainer width="100%" height={400}>
|
||||||
<LineChart
|
<LineChart data={displayData} onClick={handleClick}>
|
||||||
data={displayData}
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
onClick={handleClick}
|
<XAxis dataKey="time" />
|
||||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
<YAxis />
|
||||||
>
|
<Tooltip content={<CustomTooltip />} />
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
<Legend />
|
||||||
<XAxis
|
|
||||||
dataKey="time"
|
|
||||||
tick={{ fill: '#666' }}
|
|
||||||
tickMargin={10}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
tick={{ fill: '#666' }}
|
|
||||||
tickMargin={10}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
content={<CustomTooltip />}
|
|
||||||
cursor={{ stroke: '#ccc', strokeWidth: 1 }}
|
|
||||||
/>
|
|
||||||
{/* Убрали <Legend /> чтобы скрыть имена instance */}
|
|
||||||
{Object.keys(chartData).map((key, index) => (
|
{Object.keys(chartData).map((key, index) => (
|
||||||
<Line
|
<Line
|
||||||
key={key}
|
key={key}
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey={key}
|
dataKey={key}
|
||||||
stroke={colors[index % colors.length]}
|
stroke={colors[index % colors.length]}
|
||||||
strokeWidth={2}
|
name={key}
|
||||||
dot={false}
|
|
||||||
activeDot={{ r: 6 }}
|
|
||||||
// Убрали name чтобы не отображалось в tooltip
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</LineChart>
|
</LineChart>
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
? date.toLocaleString([], { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
|
? 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' });
|
: 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] = {};
|
if (!updatedData[key]) updatedData[key] = {};
|
||||||
updatedData[key][formattedTime] = m.value;
|
updatedData[key][formattedTime] = m.value;
|
||||||
});
|
});
|
||||||
|
|
@ -217,143 +217,41 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div>
|
||||||
backgroundColor: '#fff',
|
<div>
|
||||||
borderRadius: '8px',
|
<label htmlFor="time-range">Выберите временной диапазон: </label>
|
||||||
padding: '20px',
|
<select id="time-range" value={selectedRange.value} onChange={handleRangeChange}>
|
||||||
marginBottom: '20px'
|
{TIME_RANGES.map(range => (
|
||||||
}}>
|
<option key={range.value} value={range.value}>{range.label}</option>
|
||||||
{/* Заголовок графика */}
|
))}
|
||||||
<h3 style={{ marginTop: 0, color: '#333' }}>
|
</select>
|
||||||
</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>
|
||||||
|
<div>
|
||||||
{/* Индикатор текущего диапазона */}
|
<label>Или выберите другой диапазон: </label>
|
||||||
<div style={{
|
<div>
|
||||||
margin: '10px 0',
|
<label>Начальная дата: </label>
|
||||||
padding: '8px 12px',
|
<DatePicker
|
||||||
backgroundColor: '#f0f7ff',
|
selected={startDate}
|
||||||
borderRadius: '4px',
|
onChange={(date) => setStartDate(date)}
|
||||||
borderLeft: '3px solid #4a6baf'
|
showTimeSelect
|
||||||
}}>
|
timeFormat="HH:mm"
|
||||||
Текущий диапазон: {useCustomRange
|
timeIntervals={15}
|
||||||
? `${startDate.toLocaleString()} - ${endDate.toLocaleString()}`
|
dateFormat="yyyy-MM-dd HH:mm"
|
||||||
: selectedRange.label}
|
/>
|
||||||
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* График */}
|
|
||||||
<LineChartComponent
|
<LineChartComponent
|
||||||
chartData={chartData}
|
chartData={chartData}
|
||||||
metricName={metricName}
|
metricName={metricName}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { useDataParser } from './FlowChartComponents/DataParser';
|
||||||
import NodeWrapper from './FlowChartComponents/NodeWrapper';
|
import NodeWrapper from './FlowChartComponents/NodeWrapper';
|
||||||
|
|
||||||
const nodeTypes = {
|
const nodeTypes = {
|
||||||
customNode: NodeWrapper
|
customNode: NodeWrapper // Должно совпадать с type в useDataParser
|
||||||
};
|
};
|
||||||
|
|
||||||
const FlowChart = ({ data }) => {
|
const FlowChart = ({ data }) => {
|
||||||
|
|
@ -40,17 +40,11 @@ const FlowChart = ({ data }) => {
|
||||||
setNodes(initialNodes);
|
setNodes(initialNodes);
|
||||||
setEdges(initialEdges);
|
setEdges(initialEdges);
|
||||||
|
|
||||||
// Автоматически сворачиваем узлы, которые являются родителями последнего уровня
|
|
||||||
if (!initialized.current && data) {
|
if (!initialized.current && data) {
|
||||||
const findAndCollapseLastLevelParents = (items) => {
|
const findAndCollapseLastLevelParents = (items) => {
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
if (item.items && item.items.length > 0) {
|
if (item.items?.length > 0) {
|
||||||
// Проверяем, есть ли у детей свои дети
|
const hasGrandchildren = item.items.some(child => child.items?.length > 0);
|
||||||
const hasGrandchildren = item.items.some(child =>
|
|
||||||
child.items && child.items.length > 0
|
|
||||||
);
|
|
||||||
|
|
||||||
// Если у детей нет своих детей - это родители последнего уровня
|
|
||||||
if (!hasGrandchildren) {
|
if (!hasGrandchildren) {
|
||||||
toggleNodeCollapse(item.id);
|
toggleNodeCollapse(item.id);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -59,7 +53,6 @@ const FlowChart = ({ data }) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
findAndCollapseLastLevelParents(data.items || []);
|
findAndCollapseLastLevelParents(data.items || []);
|
||||||
initialized.current = true;
|
initialized.current = true;
|
||||||
}
|
}
|
||||||
|
|
@ -72,9 +65,7 @@ const FlowChart = ({ data }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => debouncedSetNodePositions.cancel();
|
||||||
debouncedSetNodePositions.cancel();
|
|
||||||
};
|
|
||||||
}, [debouncedSetNodePositions]);
|
}, [debouncedSetNodePositions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,7 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { isLeafNode } from './nodeUtils';
|
import { isLeafNode } from './nodeUtils';
|
||||||
import { getStatusColor } from '../dataUtils';
|
|
||||||
|
|
||||||
export const useDataParser = (nodePositions, collapsedNodes) => {
|
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) => {
|
const parseData = useCallback((data) => {
|
||||||
if (!data) return { nodes: [], edges: [] };
|
if (!data) return { nodes: [], edges: [] };
|
||||||
|
|
||||||
|
|
@ -39,7 +12,7 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
|
||||||
const baseLevelRadius = 150;
|
const baseLevelRadius = 150;
|
||||||
|
|
||||||
const traverse = (item, parentId = null, level = 0, angleStart = 0, angleEnd = 2 * Math.PI, parentRadius = 0) => {
|
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 nodeId = item.id;
|
||||||
const items = item.items || [];
|
const items = item.items || [];
|
||||||
|
|
@ -53,14 +26,15 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
|
||||||
|
|
||||||
const node = {
|
const node = {
|
||||||
id: nodeId,
|
id: nodeId,
|
||||||
type: 'customNode',
|
|
||||||
position,
|
position,
|
||||||
|
type: 'customNode', // Важно для кастомного рендеринга
|
||||||
data: {
|
data: {
|
||||||
...item,
|
|
||||||
label: item.title,
|
label: item.title,
|
||||||
style: getNodeStyle(item, isLeaf), // Переносим стили в data
|
status: item.status,
|
||||||
hasChildren: items.length > 0,
|
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}`,
|
id: `${parentId}-${nodeId}`,
|
||||||
source: parentId,
|
source: parentId,
|
||||||
target: nodeId,
|
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) {
|
if (!collapsedNodes[nodeId] && items.length > 0) {
|
||||||
const spreadAngle = angleEnd - angleStart;
|
const spreadAngle = angleEnd - angleStart;
|
||||||
items.forEach((child, index) => {
|
items.forEach((child, index) => {
|
||||||
if (!child) return;
|
|
||||||
const itemAngleStart = angleStart + (index / items.length) * spreadAngle;
|
const itemAngleStart = angleStart + (index / items.length) * spreadAngle;
|
||||||
const itemAngleEnd = angleStart + ((index + 1) / items.length) * spreadAngle;
|
const itemAngleEnd = angleStart + ((index + 1) / items.length) * spreadAngle;
|
||||||
traverse(child, nodeId, level + 1, itemAngleStart, itemAngleEnd, parentRadius + baseLevelRadius);
|
traverse(child, nodeId, level + 1, itemAngleStart, itemAngleEnd, parentRadius + baseLevelRadius);
|
||||||
|
|
@ -86,27 +62,33 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Центральный узел
|
||||||
const centerNode = {
|
const centerNode = {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
type: 'customNode', // Добавляем тип узла
|
|
||||||
position: nodePositions[data.id] || { x: centerX, y: centerY },
|
position: nodePositions[data.id] || { x: centerX, y: centerY },
|
||||||
style: getCenterNodeStyle(data),
|
type: 'customNode',
|
||||||
data: { label: data.title, hasChildren: data.items.length > 0, collapsed: collapsedNodes[data.id] }
|
data: {
|
||||||
|
label: data.title,
|
||||||
|
status: data.status,
|
||||||
|
hasChildren: data.items.length > 0,
|
||||||
|
collapsed: collapsedNodes[data.id],
|
||||||
|
isLeaf: false,
|
||||||
|
isCenterNode: true
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
nodes.push(centerNode);
|
nodes.push(centerNode);
|
||||||
|
|
||||||
|
// Обработка дочерних узлов
|
||||||
if (!collapsedNodes[data.id] && data.items.length > 0) {
|
if (!collapsedNodes[data.id] && data.items.length > 0) {
|
||||||
const angleStep = (2 * Math.PI) / data.items.length;
|
const angleStep = (2 * Math.PI) / data.items.length;
|
||||||
data.items.forEach((child, index) => {
|
data.items.forEach((child, index) => {
|
||||||
if (!child) return;
|
|
||||||
traverse(child, data.id, 1, index * angleStep, (index + 1) * angleStep, 0);
|
traverse(child, data.id, 1, index * angleStep, (index + 1) * angleStep, 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { nodes, edges };
|
return { nodes, edges };
|
||||||
}, [nodePositions, collapsedNodes, getNodeStyle, getCenterNodeStyle]);
|
}, [nodePositions, collapsedNodes]);
|
||||||
|
|
||||||
return { parseData };
|
return { parseData };
|
||||||
};
|
};
|
||||||
|
|
@ -1,63 +1,63 @@
|
||||||
import React, { memo } from 'react';
|
import React from 'react';
|
||||||
import { Handle } from 'reactflow';
|
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 (
|
return (
|
||||||
<div
|
<div style={nodeStyle}>
|
||||||
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={{
|
<div style={{
|
||||||
maxWidth: '100%',
|
padding: '0 8px',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis'
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
}}>
|
}}>
|
||||||
{data.label}
|
{data.label}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data.hasChildren && (
|
{data.hasChildren && (
|
||||||
<span style={{
|
<div style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 5,
|
top: -8,
|
||||||
right: 5,
|
right: -8,
|
||||||
fontSize: '12px',
|
width: 20,
|
||||||
cursor: 'pointer',
|
height: 20,
|
||||||
|
borderRadius: '50%',
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
padding: '2px 5px',
|
border: '1px solid #ddd',
|
||||||
borderRadius: '3px',
|
display: 'flex',
|
||||||
border: '1px solid #aaa'
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
cursor: 'pointer',
|
||||||
|
zIndex: 10
|
||||||
}}>
|
}}>
|
||||||
{data.collapsed ? '+' : '-'}
|
{data.collapsed ? '+' : '-'}
|
||||||
</span>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Хендл для исходящих соединений */}
|
|
||||||
<Handle
|
|
||||||
type="source"
|
|
||||||
position="bottom"
|
|
||||||
style={{ visibility: 'hidden' }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
export default NodeWrapper;
|
export default React.memo(NodeWrapper);
|
||||||
|
|
@ -17,6 +17,7 @@ const LoginModal = ({ onLogin, onClose }) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Отправляем данные на бэкенд
|
// Отправляем данные на бэкенд
|
||||||
|
console.log("Отправляем данные:", { username, password });
|
||||||
const response = await fetch('http://192.168.2.39:3000/auth/login', {
|
const response = await fetch('http://192.168.2.39:3000/auth/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue