diff --git a/frontend/src/GraphView.jsx b/frontend/src/GraphView.jsx index fd07c19..1d826de 100644 --- a/frontend/src/GraphView.jsx +++ b/frontend/src/GraphView.jsx @@ -3,6 +3,10 @@ import * as d3 from 'd3'; import { IconZoomIn, IconZoomOut, IconMaximize, IconMinimize } from '@tabler/icons-react'; function GraphView({ servers, connections }) { + // Debug helper (enable/disable via window.GRAPH_DEBUG = true/false in console) + const isDebug = typeof window !== 'undefined' ? (window.GRAPH_DEBUG !== false) : true; + const dbg = (...args) => { if (isDebug && typeof console !== 'undefined') console.log('[GraphView]', ...args); }; + const [nodePositions, setNodePositions] = useState({}); const [draggedNode, setDraggedNode] = useState(null); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); @@ -80,16 +84,34 @@ function GraphView({ servers, connections }) { .translateExtent([[-10000, -10000], [10000, 10000]]) .on('zoom', (event) => { g.attr('transform', event.transform); + // dbg('onZoom', { k: event.transform.k, x: event.transform.x, y: event.transform.y }); }); svg.call(zoomRef.current); + dbg('zoom initialized', { width, height }); }, []); // Контролы масштабирования const zoomBy = (factor) => { if (!zoomRef.current || !svgRef.current) return; - const svg = d3.select(svgRef.current); - // Используем встроенную математику d3.zoom с учётом extent - svg.transition().duration(220).call(zoomRef.current.scaleBy, factor); + const svgEl = svgRef.current; + const current = d3.zoomTransform(svgEl); + const cx = svgEl.clientWidth / 2; + const cy = svgEl.clientHeight / 2; + dbg('zoomBy click', { factor, current: { k: current.k, x: current.x, y: current.y }, center: { cx, cy } }); + const next = d3.zoomIdentity + .translate(current.x, current.y) + .scale(current.k) + .translate(cx, cy) + .scale(factor) + .translate(-cx, -cy); + d3.select(svgEl) + .transition() + .duration(220) + .call(zoomRef.current.transform, next) + .on('end', () => { + const nt = d3.zoomTransform(svgEl); + dbg('zoomBy end', { next: { k: nt.k, x: nt.x, y: nt.y } }); + }); }; const fitToView = () => { @@ -127,6 +149,7 @@ function GraphView({ servers, connections }) { const toggleFullscreen = () => { const el = containerRef.current; if (!el) return; + dbg('toggleFullscreen', { isFullscreen: Boolean(document.fullscreenElement) }); if (!document.fullscreenElement) { el.requestFullscreen?.(); } else { @@ -236,11 +259,13 @@ function GraphView({ servers, connections }) { return map[tunnelType] || { color: '#667382', dash: '5,5', width: 2 }; }; - // Флаг страны по ISO-2 (emoji). Для не-ISO-2 вернём код как есть - const getFlagEmoji = (countryCode) => { - if (!countryCode) return ''; - const cc = String(countryCode).trim().toUpperCase(); - if (cc.length !== 2) return cc; // например, SWE + // Флаг страны по ISO-2 с поддержкой нестандартных кодов из списка (SWE->SE) + const getFlagEmoji = (code) => { + if (!code) return ''; + const map = { SWE: 'SE', UK: 'GB' }; + const ccRaw = String(code).trim().toUpperCase(); + const cc = (map[ccRaw] || ccRaw).slice(0, 2); + if (cc.length !== 2) return ccRaw; return cc.replace(/./g, (ch) => String.fromCodePoint(127397 + ch.charCodeAt())); }; @@ -429,21 +454,29 @@ function GraphView({ servers, connections }) { y={badgeY} rx={8} ry={8} - width={36} - height={18} + width={52} + height={20} fill="#fff" stroke={countryColor} /> {flag} + + {String(server.country).toUpperCase().slice(0,3)} + ); })()}