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

This commit is contained in:
2025-07-15 21:45:28 +07:00
parent c5da9e357e
commit a104cc7a3f
4 changed files with 176 additions and 283 deletions
+157 -122
View File
@@ -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>
);
}