feat: Implement force-directed layout and zoom functionality in GraphView for enhanced visualization and interactivity
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m58s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m58s
This commit is contained in:
+71
-19
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import * as d3 from 'd3';
|
||||
|
||||
function GraphView({ servers, connections }) {
|
||||
const [nodePositions, setNodePositions] = useState({});
|
||||
@@ -6,6 +7,47 @@ function GraphView({ servers, connections }) {
|
||||
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
||||
const [hoveredLinkId, setHoveredLinkId] = useState(null);
|
||||
const svgRef = useRef(null);
|
||||
const gRef = useRef(null);
|
||||
|
||||
// Автоматическая раскладка (force-directed)
|
||||
useEffect(() => {
|
||||
if (!servers || servers.length === 0) return;
|
||||
|
||||
const nodes = servers.map((s) => ({ id: s.ip }));
|
||||
const links = (connections || []).map((c) => ({ source: c.from, target: c.to, type: c.tunnelType }));
|
||||
|
||||
const distanceForType = (t) => ({ GRE: 200, IPSec: 220, WireGuard: 180, OpenVPN: 200 }[t] || 210);
|
||||
|
||||
const simulation = d3
|
||||
.forceSimulation(nodes)
|
||||
.force('link', d3.forceLink(links).id((d) => d.id).distance((d) => distanceForType(d.type)))
|
||||
.force('charge', d3.forceManyBody().strength(-500))
|
||||
.force('collide', d3.forceCollide(70))
|
||||
.force('center', d3.forceCenter(450, 300))
|
||||
.stop();
|
||||
|
||||
for (let i = 0; i < 300; i += 1) simulation.tick();
|
||||
|
||||
const pos = {};
|
||||
nodes.forEach((n) => {
|
||||
pos[n.id] = { x: n.x, y: n.y };
|
||||
});
|
||||
setNodePositions(pos);
|
||||
}, [servers, connections]);
|
||||
|
||||
// Зум/панорамирование
|
||||
useEffect(() => {
|
||||
const svg = d3.select(svgRef.current);
|
||||
const g = d3.select(gRef.current);
|
||||
svg.call(
|
||||
d3
|
||||
.zoom()
|
||||
.scaleExtent([0.5, 2])
|
||||
.on('zoom', (event) => {
|
||||
g.attr('transform', event.transform);
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
if (servers.length === 0) {
|
||||
return (
|
||||
@@ -36,11 +78,11 @@ function GraphView({ servers, connections }) {
|
||||
|
||||
// Обработчик начала перетаскивания
|
||||
const handleMouseDown = (e, serverIp) => {
|
||||
const svg = svgRef.current;
|
||||
const pt = svg.createSVGPoint();
|
||||
const target = gRef.current || svgRef.current;
|
||||
const pt = (gRef.current?.ownerSVGElement || svgRef.current).createSVGPoint();
|
||||
pt.x = e.clientX;
|
||||
pt.y = e.clientY;
|
||||
const svgP = pt.matrixTransform(svg.getScreenCTM().inverse());
|
||||
const svgP = pt.matrixTransform(target.getScreenCTM().inverse());
|
||||
|
||||
const currentPos = nodePositions[serverIp] || getInitialNodePosition(
|
||||
servers.findIndex(s => s.ip === serverIp),
|
||||
@@ -57,12 +99,11 @@ function GraphView({ servers, connections }) {
|
||||
// Обработчик перетаскивания
|
||||
const handleMouseMove = (e) => {
|
||||
if (!draggedNode) return;
|
||||
|
||||
const svg = svgRef.current;
|
||||
const pt = svg.createSVGPoint();
|
||||
const target = gRef.current || svgRef.current;
|
||||
const pt = (gRef.current?.ownerSVGElement || svgRef.current).createSVGPoint();
|
||||
pt.x = e.clientX;
|
||||
pt.y = e.clientY;
|
||||
const svgP = pt.matrixTransform(svg.getScreenCTM().inverse());
|
||||
const svgP = pt.matrixTransform(target.getScreenCTM().inverse());
|
||||
|
||||
setNodePositions(prev => ({
|
||||
...prev,
|
||||
@@ -148,6 +189,7 @@ function GraphView({ servers, connections }) {
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<g ref={gRef}>
|
||||
{/* Связи */}
|
||||
{connections.map((connection, index) => {
|
||||
const fromServer = servers.find(s => s.ip === connection.from);
|
||||
@@ -161,17 +203,26 @@ function GraphView({ servers, connections }) {
|
||||
const id = `${connection.from}-${connection.to}-${index}`;
|
||||
const isHovered = hoveredLinkId === id;
|
||||
|
||||
// Кривая линия с небольшим отступом от прямой для читаемости
|
||||
const mx = (fromPos.x + toPos.x) / 2;
|
||||
const my = (fromPos.y + toPos.y) / 2;
|
||||
const dx = toPos.x - fromPos.x;
|
||||
const dy = toPos.y - fromPos.y;
|
||||
const len = Math.sqrt(dx * dx + dy * dy) || 1;
|
||||
const nx = (-dy / len) * 30; // перпендикуляр, 30px
|
||||
const ny = (dx / len) * 30;
|
||||
const cx = mx + nx;
|
||||
const cy = my + ny;
|
||||
|
||||
return (
|
||||
<g key={`link-${index}`}
|
||||
onMouseEnter={() => setHoveredLinkId(id)}
|
||||
onMouseLeave={() => setHoveredLinkId(null)}
|
||||
>
|
||||
{/* Линия связи */}
|
||||
<line
|
||||
x1={fromPos.x}
|
||||
y1={fromPos.y}
|
||||
x2={toPos.x}
|
||||
y2={toPos.y}
|
||||
<path
|
||||
d={`M ${fromPos.x} ${fromPos.y} Q ${cx} ${cy} ${toPos.x} ${toPos.y}`}
|
||||
fill="none"
|
||||
stroke={style.color}
|
||||
strokeWidth={isHovered ? style.width + 1 : style.width}
|
||||
strokeDasharray={style.dash}
|
||||
@@ -182,8 +233,8 @@ function GraphView({ servers, connections }) {
|
||||
|
||||
{/* Подпись связи */}
|
||||
<rect
|
||||
x={(fromPos.x + toPos.x) / 2 - 40}
|
||||
y={(fromPos.y + toPos.y) / 2 - 15}
|
||||
x={mx - 50}
|
||||
y={my - 35}
|
||||
width="80"
|
||||
height="30"
|
||||
rx="15"
|
||||
@@ -192,8 +243,8 @@ function GraphView({ servers, connections }) {
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<text
|
||||
x={(fromPos.x + toPos.x) / 2}
|
||||
y={(fromPos.y + toPos.y) / 2}
|
||||
x={mx - 10}
|
||||
y={my - 18}
|
||||
textAnchor="middle"
|
||||
fontSize="11"
|
||||
fill="#374151"
|
||||
@@ -202,8 +253,8 @@ function GraphView({ servers, connections }) {
|
||||
{connection.tunnelType}
|
||||
</text>
|
||||
<text
|
||||
x={(fromPos.x + toPos.x) / 2}
|
||||
y={(fromPos.y + toPos.y) / 2 + 15}
|
||||
x={mx}
|
||||
y={my - 3}
|
||||
textAnchor="middle"
|
||||
fontSize="9"
|
||||
fill="#6b7280"
|
||||
@@ -329,6 +380,7 @@ function GraphView({ servers, connections }) {
|
||||
})}
|
||||
|
||||
{/* Доп. определения добавлены выше */}
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user