feat: Replace react-vis-network-graph with react-sigma for enhanced graph visualization and update GraphView component to utilize SVG for rendering connections and nodes
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 8m11s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 8m11s
This commit is contained in:
+157
-122
@@ -1,124 +1,43 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Network } from 'react-vis-network-graph';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
function GraphView({ servers, connections }) {
|
||||
const graph = useMemo(() => {
|
||||
const nodes = servers.map(server => ({
|
||||
id: server.ip,
|
||||
label: `${server.dns}\n${server.ip}`,
|
||||
title: `${server.dns} (${server.ip})\n${server.country} - ${server.provider}`,
|
||||
color: {
|
||||
background: '#3b82f6',
|
||||
border: '#1e40af',
|
||||
highlight: {
|
||||
background: '#60a5fa',
|
||||
border: '#3b82f6'
|
||||
}
|
||||
},
|
||||
font: {
|
||||
color: '#ffffff',
|
||||
size: 14,
|
||||
face: 'Arial'
|
||||
},
|
||||
shape: 'box',
|
||||
borderWidth: 2,
|
||||
shadow: true
|
||||
}));
|
||||
const [positions, setPositions] = useState({});
|
||||
const [dimensions, setDimensions] = useState({ width: 800, height: 500 });
|
||||
const svgRef = useRef(null);
|
||||
|
||||
const edges = connections.map((connection, index) => ({
|
||||
id: index,
|
||||
from: connection.from,
|
||||
to: connection.to,
|
||||
label: `${connection.tunnelType}\n${connection.ipA} ⇄ ${connection.ipB}`,
|
||||
title: `${connection.tunnelType} туннель\n${connection.ipA} ⇄ ${connection.ipB}`,
|
||||
color: {
|
||||
color: '#64748b',
|
||||
highlight: '#3b82f6',
|
||||
hover: '#3b82f6'
|
||||
},
|
||||
font: {
|
||||
color: '#374151',
|
||||
size: 12,
|
||||
face: 'Arial',
|
||||
align: 'middle'
|
||||
},
|
||||
width: 2,
|
||||
arrows: {
|
||||
to: {
|
||||
enabled: true,
|
||||
scaleFactor: 0.8
|
||||
}
|
||||
},
|
||||
smooth: {
|
||||
type: 'curvedCW',
|
||||
roundness: 0.2
|
||||
}
|
||||
}));
|
||||
// Вычисляем позиции узлов
|
||||
useEffect(() => {
|
||||
if (servers.length === 0) return;
|
||||
|
||||
return { nodes, edges };
|
||||
}, [servers, connections]);
|
||||
const newPositions = {};
|
||||
const centerX = dimensions.width / 2;
|
||||
const centerY = dimensions.height / 2;
|
||||
const radius = Math.min(dimensions.width, dimensions.height) * 0.3;
|
||||
|
||||
const options = {
|
||||
nodes: {
|
||||
shape: 'box',
|
||||
margin: 10,
|
||||
shadow: true,
|
||||
font: {
|
||||
size: 14,
|
||||
face: 'Arial'
|
||||
}
|
||||
},
|
||||
edges: {
|
||||
font: {
|
||||
size: 12,
|
||||
align: 'middle'
|
||||
},
|
||||
smooth: {
|
||||
type: 'curvedCW',
|
||||
roundness: 0.2
|
||||
}
|
||||
},
|
||||
physics: {
|
||||
enabled: true,
|
||||
solver: 'forceAtlas2Based',
|
||||
forceAtlas2Based: {
|
||||
gravitationalConstant: -50,
|
||||
centralGravity: 0.01,
|
||||
springLength: 200,
|
||||
springConstant: 0.08,
|
||||
damping: 0.4
|
||||
},
|
||||
stabilization: {
|
||||
enabled: true,
|
||||
iterations: 1000,
|
||||
updateInterval: 100
|
||||
}
|
||||
},
|
||||
interaction: {
|
||||
hover: true,
|
||||
tooltipDelay: 200,
|
||||
zoomView: true,
|
||||
dragView: true
|
||||
},
|
||||
layout: {
|
||||
improvedLayout: true,
|
||||
hierarchical: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
};
|
||||
servers.forEach((server, index) => {
|
||||
const angle = (index / servers.length) * 2 * Math.PI;
|
||||
newPositions[server.ip] = {
|
||||
x: centerX + radius * Math.cos(angle),
|
||||
y: centerY + radius * Math.sin(angle)
|
||||
};
|
||||
});
|
||||
|
||||
const events = {
|
||||
select: function(event) {
|
||||
const { nodes, edges } = event;
|
||||
console.log('Selected nodes:', nodes);
|
||||
console.log('Selected edges:', edges);
|
||||
},
|
||||
hoverNode: function(event) {
|
||||
const { node } = event;
|
||||
console.log('Hovered node:', node);
|
||||
}
|
||||
};
|
||||
setPositions(newPositions);
|
||||
}, [servers, dimensions]);
|
||||
|
||||
// Обработчик изменения размера
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (svgRef.current) {
|
||||
const rect = svgRef.current.getBoundingClientRect();
|
||||
setDimensions({ width: rect.width, height: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
if (servers.length === 0) {
|
||||
return (
|
||||
@@ -132,13 +51,129 @@ function GraphView({ servers, connections }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', height: '500px', border: '1px solid #e5e7eb', borderRadius: '8px' }}>
|
||||
<Network
|
||||
graph={graph}
|
||||
options={options}
|
||||
events={events}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
/>
|
||||
<div style={{ width: '100%', height: '500px', border: '1px solid #e5e7eb', borderRadius: '8px', overflow: 'hidden' }}>
|
||||
<svg
|
||||
ref={svgRef}
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox={`0 0 ${dimensions.width} ${dimensions.height}`}
|
||||
style={{ background: '#f8fafc' }}
|
||||
>
|
||||
{/* Связи */}
|
||||
{connections.map((connection, index) => {
|
||||
const fromPos = positions[connection.from];
|
||||
const toPos = positions[connection.to];
|
||||
|
||||
if (!fromPos || !toPos) return null;
|
||||
|
||||
return (
|
||||
<g key={`link-${index}`}>
|
||||
{/* Линия связи */}
|
||||
<line
|
||||
x1={fromPos.x}
|
||||
y1={fromPos.y}
|
||||
x2={toPos.x}
|
||||
y2={toPos.y}
|
||||
stroke="#64748b"
|
||||
strokeWidth="2"
|
||||
markerEnd="url(#arrowhead)"
|
||||
/>
|
||||
|
||||
{/* Подпись связи */}
|
||||
<text
|
||||
x={(fromPos.x + toPos.x) / 2}
|
||||
y={(fromPos.y + toPos.y) / 2 - 10}
|
||||
textAnchor="middle"
|
||||
fontSize="12"
|
||||
fill="#374151"
|
||||
fontWeight="bold"
|
||||
>
|
||||
{connection.tunnelType}
|
||||
</text>
|
||||
<text
|
||||
x={(fromPos.x + toPos.x) / 2}
|
||||
y={(fromPos.y + toPos.y) / 2 + 5}
|
||||
textAnchor="middle"
|
||||
fontSize="10"
|
||||
fill="#6b7280"
|
||||
>
|
||||
{connection.ipA} ⇄ {connection.ipB}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Узлы (серверы) */}
|
||||
{servers.map(server => {
|
||||
const pos = positions[server.ip];
|
||||
if (!pos) return null;
|
||||
|
||||
return (
|
||||
<g key={server.ip}>
|
||||
{/* Круг узла */}
|
||||
<circle
|
||||
cx={pos.x}
|
||||
cy={pos.y}
|
||||
r="25"
|
||||
fill="#3b82f6"
|
||||
stroke="#1e40af"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
|
||||
{/* DNS имя */}
|
||||
<text
|
||||
x={pos.x}
|
||||
y={pos.y - 5}
|
||||
textAnchor="middle"
|
||||
fontSize="11"
|
||||
fill="white"
|
||||
fontWeight="bold"
|
||||
>
|
||||
{server.dns}
|
||||
</text>
|
||||
|
||||
{/* IP адрес */}
|
||||
<text
|
||||
x={pos.x}
|
||||
y={pos.y + 10}
|
||||
textAnchor="middle"
|
||||
fontSize="10"
|
||||
fill="white"
|
||||
>
|
||||
{server.ip}
|
||||
</text>
|
||||
|
||||
{/* Страна */}
|
||||
<text
|
||||
x={pos.x}
|
||||
y={pos.y + 25}
|
||||
textAnchor="middle"
|
||||
fontSize="9"
|
||||
fill="#6b7280"
|
||||
>
|
||||
{server.country}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Определение стрелки */}
|
||||
<defs>
|
||||
<marker
|
||||
id="arrowhead"
|
||||
markerWidth="10"
|
||||
markerHeight="7"
|
||||
refX="9"
|
||||
refY="3.5"
|
||||
orient="auto"
|
||||
>
|
||||
<polygon
|
||||
points="0 0, 10 3.5, 0 7"
|
||||
fill="#64748b"
|
||||
/>
|
||||
</marker>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user