feat: Replace ForceGraph with react-vis-network-graph for improved visualization and enhance ServerManager with connection validation and deletion functionality
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 7m50s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 7m50s
This commit is contained in:
+133
-63
@@ -1,73 +1,143 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import ForceGraph2D from 'react-force-graph-2d';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Network } from 'react-vis-network-graph';
|
||||
|
||||
function GraphView({ servers, connections }) {
|
||||
// Преобразуем данные в формат, подходящий для ForceGraph
|
||||
const nodes = servers.map(s => ({ id: s.ip, label: s.dns || s.ip }));
|
||||
const links = connections.map(c => ({
|
||||
source: c.from,
|
||||
target: c.to,
|
||||
tunnelType: c.tunnelType,
|
||||
ipA: c.ipA,
|
||||
ipB: c.ipB
|
||||
}));
|
||||
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 fgRef = useRef();
|
||||
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 (fgRef.current) {
|
||||
fgRef.current.d3ReheatSimulation();
|
||||
return { nodes, edges };
|
||||
}, [servers, connections]);
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}, [nodes.length, links.length]);
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
if (servers.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-5">
|
||||
<div className="text-muted">
|
||||
<h4>Нет серверов для отображения</h4>
|
||||
<p>Добавьте серверы, чтобы увидеть граф связей</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', height: 400 }}>
|
||||
<ForceGraph2D
|
||||
ref={fgRef}
|
||||
graphData={{ nodes, links }}
|
||||
nodeLabel={node => node.label}
|
||||
nodeAutoColorBy="id"
|
||||
linkDirectionalArrowLength={6}
|
||||
linkDirectionalArrowRelPos={1}
|
||||
linkLabel={l => `${l.tunnelType}: ${l.ipA} ⇄ ${l.ipB}`}
|
||||
linkWidth={2}
|
||||
linkColor={() => '#888'}
|
||||
nodeCanvasObject={(node, ctx, globalScale) => {
|
||||
const label = node.label;
|
||||
const fontSize = 14/globalScale;
|
||||
ctx.font = `${fontSize}px sans-serif`;
|
||||
ctx.fillStyle = '#1e293b';
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, 10, 0, 2 * Math.PI, false);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(label, node.x, node.y - 18);
|
||||
}}
|
||||
linkCanvasObjectMode={() => 'after'}
|
||||
linkCanvasObject={(link, ctx, globalScale) => {
|
||||
const label = `${link.tunnelType}: ${link.ipA} ⇄ ${link.ipB}`;
|
||||
if (!label) return;
|
||||
const start = link.source;
|
||||
const end = link.target;
|
||||
if (typeof start !== 'object' || typeof end !== 'object') return;
|
||||
const textPos = {
|
||||
x: (start.x + end.x) / 2,
|
||||
y: (start.y + end.y) / 2
|
||||
};
|
||||
ctx.save();
|
||||
ctx.font = `${12/globalScale}px sans-serif`;
|
||||
ctx.fillStyle = '#2563eb';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(label, textPos.x, textPos.y);
|
||||
ctx.restore();
|
||||
}}
|
||||
<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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user