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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m40s
This commit is contained in:
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user