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 zoomRef = useRef(null);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
const [transformState, setTransformState] = useState({ k: 1, x: 0, y: 0 });
|
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_MIN = 0.2;
|
||||||
const ZOOM_MAX = 4;
|
const ZOOM_MAX = 4;
|
||||||
@@ -90,8 +92,9 @@ function GraphView({ servers, connections }) {
|
|||||||
.extent([[0, 0], [width, height]])
|
.extent([[0, 0], [width, height]])
|
||||||
.translateExtent([[-10000, -10000], [10000, 10000]])
|
.translateExtent([[-10000, -10000], [10000, 10000]])
|
||||||
.on('zoom', (event) => {
|
.on('zoom', (event) => {
|
||||||
g.attr('transform', event.transform);
|
const t = event.transform;
|
||||||
setTransformState({ k: event.transform.k, x: event.transform.x, y: event.transform.y });
|
g.attr('transform', `translate(${t.x},${t.y}) scale(${t.k})`);
|
||||||
|
setTransformState({ k: t.k, x: t.x, y: t.y });
|
||||||
});
|
});
|
||||||
zoomRef.current = zoom;
|
zoomRef.current = zoom;
|
||||||
svg
|
svg
|
||||||
@@ -100,6 +103,30 @@ function GraphView({ servers, connections }) {
|
|||||||
dbg('zoom initialized', { width, height });
|
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) => {
|
const zoomBy = (factor) => {
|
||||||
if (!zoomRef.current || !svgRef.current) return;
|
if (!zoomRef.current || !svgRef.current) return;
|
||||||
@@ -222,6 +249,8 @@ function GraphView({ servers, connections }) {
|
|||||||
|
|
||||||
// Обработчик начала перетаскивания
|
// Обработчик начала перетаскивания
|
||||||
const handleMouseDown = (e, serverIp) => {
|
const handleMouseDown = (e, serverIp) => {
|
||||||
|
// В режиме панорамирования — не начинаем перетаскивание узла, даём d3.zoom обработать drag
|
||||||
|
if (panModeRef.current) return;
|
||||||
const target = gRef.current || svgRef.current;
|
const target = gRef.current || svgRef.current;
|
||||||
const pt = (gRef.current?.ownerSVGElement || svgRef.current).createSVGPoint();
|
const pt = (gRef.current?.ownerSVGElement || svgRef.current).createSVGPoint();
|
||||||
pt.x = e.clientX;
|
pt.x = e.clientX;
|
||||||
@@ -311,11 +340,10 @@ function GraphView({ servers, connections }) {
|
|||||||
ref={svgRef}
|
ref={svgRef}
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
viewBox="0 0 900 600"
|
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
onMouseUp={handleMouseUp}
|
onMouseUp={handleMouseUp}
|
||||||
onMouseLeave={handleMouseUp}
|
onMouseLeave={handleMouseUp}
|
||||||
style={{ cursor: draggedNode ? 'grabbing' : 'default' }}
|
style={{ cursor: (draggedNode || isPanningMode) ? 'grabbing' : 'grab' }}
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
{/* Лёгкая сетка фона */}
|
{/* Лёгкая сетка фона */}
|
||||||
@@ -355,9 +383,9 @@ function GraphView({ servers, connections }) {
|
|||||||
<feDropShadow dx="0" dy="1" stdDeviation="2" floodOpacity="0.25" />
|
<feDropShadow dx="0" dy="1" stdDeviation="2" floodOpacity="0.25" />
|
||||||
</filter>
|
</filter>
|
||||||
</defs>
|
</defs>
|
||||||
{/* Подложка сетки */}
|
|
||||||
<rect width="100%" height="100%" fill="url(#grid)" />
|
|
||||||
<g ref={gRef}>
|
<g ref={gRef}>
|
||||||
|
{/* Подложка сетки (внутри слоя зума) */}
|
||||||
|
<rect x={-10000} y={-10000} width={20000} height={20000} fill="url(#grid)" />
|
||||||
{/* Связи */}
|
{/* Связи */}
|
||||||
{connections.map((connection, index) => {
|
{connections.map((connection, index) => {
|
||||||
const fromServer = servers.find(s => s.ip === connection.from);
|
const fromServer = servers.find(s => s.ip === connection.from);
|
||||||
@@ -397,7 +425,6 @@ function GraphView({ servers, connections }) {
|
|||||||
markerEnd={`url(#arrow-${connection.tunnelType})`}
|
markerEnd={`url(#arrow-${connection.tunnelType})`}
|
||||||
filter={isHovered ? 'url(#glow)' : undefined}
|
filter={isHovered ? 'url(#glow)' : undefined}
|
||||||
opacity={isHovered ? 1 : 0.9}
|
opacity={isHovered ? 1 : 0.9}
|
||||||
style={{ vectorEffect: 'non-scaling-stroke' }}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Подпись связи: ярлык с подложкой и halo */}
|
{/* Подпись связи: ярлык с подложкой и halo */}
|
||||||
|
|||||||
Reference in New Issue
Block a user