diff --git a/frontend/src/GraphView.jsx b/frontend/src/GraphView.jsx index 3ddb1f5..fa5cdac 100644 --- a/frontend/src/GraphView.jsx +++ b/frontend/src/GraphView.jsx @@ -8,6 +8,11 @@ function GraphView({ servers, connections }) { const [hoveredLinkId, setHoveredLinkId] = useState(null); const svgRef = useRef(null); const gRef = useRef(null); + const zoomRef = useRef(null); + + // Размеры карточки узла + const NODE_WIDTH = 160; + const NODE_HEIGHT = 74; // Автоматическая раскладка (force-directed) useEffect(() => { @@ -33,20 +38,42 @@ function GraphView({ servers, connections }) { pos[n.id] = { x: n.x, y: n.y }; }); setNodePositions(pos); + + // После раскладки подгоняем граф по области видимости + try { + const svgEl = svgRef.current; + const box = svgEl.getBoundingClientRect(); + const padding = 40; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + nodes.forEach((n) => { + minX = Math.min(minX, n.x - NODE_WIDTH / 2); + minY = Math.min(minY, n.y - NODE_HEIGHT / 2); + maxX = Math.max(maxX, n.x + NODE_WIDTH / 2); + maxY = Math.max(maxY, n.y + NODE_HEIGHT / 2); + }); + const contentW = Math.max(1, maxX - minX); + const contentH = Math.max(1, maxY - minY); + const scale = Math.max(0.5, Math.min(1.4, Math.min((box.width - padding) / contentW, (box.height - padding) / contentH))); + const tx = (box.width - scale * (minX + maxX)) / 2; + const ty = (box.height - scale * (minY + maxY)) / 2; + const t = d3.zoomIdentity.translate(tx, ty).scale(scale); + d3.select(svgEl).transition().duration(350).call(zoomRef.current.transform, t); + } catch (e) { + // ignore + } }, [servers, connections]); // Зум/панорамирование useEffect(() => { const svg = d3.select(svgRef.current); const g = d3.select(gRef.current); - svg.call( - d3 - .zoom() + zoomRef.current = d3 + .zoom() .scaleExtent([0.5, 2]) .on('zoom', (event) => { g.attr('transform', event.transform); - }) - ); + }); + svg.call(zoomRef.current); }, []); if (servers.length === 0) { @@ -164,6 +191,10 @@ function GraphView({ servers, connections }) { style={{ cursor: draggedNode ? 'grabbing' : 'default' }} > + {/* Лёгкая сетка фона */} + + + {/* Определение стрелок под цвет канала */} {Array.from(new Set(connections.map(c => c.tunnelType))).map((t) => { const style = getTunnelStyle(t); @@ -188,7 +219,13 @@ function GraphView({ servers, connections }) { + {/* Тень карточки */} + + + + {/* Подложка сетки */} + {/* Связи */} {connections.map((connection, index) => { @@ -229,6 +266,7 @@ function GraphView({ servers, connections }) { markerEnd={`url(#arrow-${connection.tunnelType})`} filter={isHovered ? 'url(#glow)' : undefined} opacity={isHovered ? 1 : 0.9} + style={{ vectorEffect: 'non-scaling-stroke' }} /> {/* Подпись связи */} @@ -242,7 +280,7 @@ function GraphView({ servers, connections }) { stroke={style.color} strokeWidth="1" /> - - {/* Тень */} - - - {/* Основной круг */} - handleMouseDown(e, server.ip)} /> - - {/* Внутренний круг с цветом страны */} - - - {/* Иконка сервера */} - 🖥️ - - - {/* DNS имя */} - + {server.country} + + {/* Название */} + {server.dns} - - {/* IP адрес */} + {/* IP */} {server.ip} - {/* Провайдер */} - {server.provider} - - - {/* Бейдж страны */} - - - {server.country} + {server.provider} );