diff --git a/frontend/src/GraphView.jsx b/frontend/src/GraphView.jsx
index 2a8700f..d10189f 100644
--- a/frontend/src/GraphView.jsx
+++ b/frontend/src/GraphView.jsx
@@ -17,6 +17,8 @@ function GraphView({ servers, connections }) {
const zoomRef = useRef(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const [transformState, setTransformState] = useState({ k: 1, x: 0, y: 0 });
+ const [isPanningMode, setIsPanningMode] = useState(false);
+ const panModeRef = useRef(false);
const ZOOM_MIN = 0.2;
const ZOOM_MAX = 4;
@@ -90,8 +92,9 @@ function GraphView({ servers, connections }) {
.extent([[0, 0], [width, height]])
.translateExtent([[-10000, -10000], [10000, 10000]])
.on('zoom', (event) => {
- g.attr('transform', event.transform);
- setTransformState({ k: event.transform.k, x: event.transform.x, y: event.transform.y });
+ const t = event.transform;
+ g.attr('transform', `translate(${t.x},${t.y}) scale(${t.k})`);
+ setTransformState({ k: t.k, x: t.x, y: t.y });
});
zoomRef.current = zoom;
svg
@@ -100,6 +103,30 @@ function GraphView({ servers, connections }) {
dbg('zoom initialized', { width, height });
}, []);
+ // Горячая клавиша: пробел включает режим панорамирования (чтобы тянуть в любом месте, даже на узлах)
+ useEffect(() => {
+ const down = (e) => {
+ if (e.code === 'Space') {
+ e.preventDefault();
+ setIsPanningMode(true);
+ panModeRef.current = true;
+ }
+ };
+ const up = (e) => {
+ if (e.code === 'Space') {
+ e.preventDefault();
+ setIsPanningMode(false);
+ panModeRef.current = false;
+ }
+ };
+ window.addEventListener('keydown', down, { passive: false });
+ window.addEventListener('keyup', up, { passive: false });
+ return () => {
+ window.removeEventListener('keydown', down);
+ window.removeEventListener('keyup', up);
+ };
+ }, []);
+
// Контролы масштабирования
const zoomBy = (factor) => {
if (!zoomRef.current || !svgRef.current) return;
@@ -222,6 +249,8 @@ function GraphView({ servers, connections }) {
// Обработчик начала перетаскивания
const handleMouseDown = (e, serverIp) => {
+ // В режиме панорамирования — не начинаем перетаскивание узла, даём d3.zoom обработать drag
+ if (panModeRef.current) return;
const target = gRef.current || svgRef.current;
const pt = (gRef.current?.ownerSVGElement || svgRef.current).createSVGPoint();
pt.x = e.clientX;
@@ -311,11 +340,10 @@ function GraphView({ servers, connections }) {
ref={svgRef}
width="100%"
height="100%"
- viewBox="0 0 900 600"
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
- style={{ cursor: draggedNode ? 'grabbing' : 'default' }}
+ style={{ cursor: (draggedNode || isPanningMode) ? 'grabbing' : 'grab' }}
>
{/* Лёгкая сетка фона */}
@@ -355,9 +383,9 @@ function GraphView({ servers, connections }) {
- {/* Подложка сетки */}
-
+ {/* Подложка сетки (внутри слоя зума) */}
+
{/* Связи */}
{connections.map((connection, index) => {
const fromServer = servers.find(s => s.ip === connection.from);
@@ -397,7 +425,6 @@ function GraphView({ servers, connections }) {
markerEnd={`url(#arrow-${connection.tunnelType})`}
filter={isHovered ? 'url(#glow)' : undefined}
opacity={isHovered ? 1 : 0.9}
- style={{ vectorEffect: 'non-scaling-stroke' }}
/>
{/* Подпись связи: ярлык с подложкой и halo */}