feat: Implement panning mode in GraphView, allowing users to pan the graph using the spacebar for improved navigation and interaction
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m40s

This commit is contained in:
2025-08-08 19:34:48 +07:00
parent b1b043f7bc
commit 522112cc3c
+34 -7
View File
@@ -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' }}
>
<defs>
{/* Лёгкая сетка фона */}
@@ -355,9 +383,9 @@ function GraphView({ servers, connections }) {
<feDropShadow dx="0" dy="1" stdDeviation="2" floodOpacity="0.25" />
</filter>
</defs>
{/* Подложка сетки */}
<rect width="100%" height="100%" fill="url(#grid)" />
<g ref={gRef}>
{/* Подложка сетки (внутри слоя зума) */}
<rect x={-10000} y={-10000} width={20000} height={20000} fill="url(#grid)" />
{/* Связи */}
{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 */}