diff --git a/frontend/src/GraphView.jsx b/frontend/src/GraphView.jsx
index 3ddb1f5..fa5cdac 100644
--- a/frontend/src/GraphView.jsx
+++ b/frontend/src/GraphView.jsx
@@ -8,6 +8,11 @@ function GraphView({ servers, connections }) {
const [hoveredLinkId, setHoveredLinkId] = useState(null);
const svgRef = useRef(null);
const gRef = useRef(null);
+ const zoomRef = useRef(null);
+
+ // Размеры карточки узла
+ const NODE_WIDTH = 160;
+ const NODE_HEIGHT = 74;
// Автоматическая раскладка (force-directed)
useEffect(() => {
@@ -33,20 +38,42 @@ function GraphView({ servers, connections }) {
pos[n.id] = { x: n.x, y: n.y };
});
setNodePositions(pos);
+
+ // После раскладки подгоняем граф по области видимости
+ try {
+ const svgEl = svgRef.current;
+ const box = svgEl.getBoundingClientRect();
+ const padding = 40;
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
+ nodes.forEach((n) => {
+ minX = Math.min(minX, n.x - NODE_WIDTH / 2);
+ minY = Math.min(minY, n.y - NODE_HEIGHT / 2);
+ maxX = Math.max(maxX, n.x + NODE_WIDTH / 2);
+ maxY = Math.max(maxY, n.y + NODE_HEIGHT / 2);
+ });
+ const contentW = Math.max(1, maxX - minX);
+ const contentH = Math.max(1, maxY - minY);
+ const scale = Math.max(0.5, Math.min(1.4, Math.min((box.width - padding) / contentW, (box.height - padding) / contentH)));
+ const tx = (box.width - scale * (minX + maxX)) / 2;
+ const ty = (box.height - scale * (minY + maxY)) / 2;
+ const t = d3.zoomIdentity.translate(tx, ty).scale(scale);
+ d3.select(svgEl).transition().duration(350).call(zoomRef.current.transform, t);
+ } catch (e) {
+ // ignore
+ }
}, [servers, connections]);
// Зум/панорамирование
useEffect(() => {
const svg = d3.select(svgRef.current);
const g = d3.select(gRef.current);
- svg.call(
- d3
- .zoom()
+ zoomRef.current = d3
+ .zoom()
.scaleExtent([0.5, 2])
.on('zoom', (event) => {
g.attr('transform', event.transform);
- })
- );
+ });
+ svg.call(zoomRef.current);
}, []);
if (servers.length === 0) {
@@ -164,6 +191,10 @@ function GraphView({ servers, connections }) {
style={{ cursor: draggedNode ? 'grabbing' : 'default' }}
>
+ {/* Лёгкая сетка фона */}
+
+
+
{/* Определение стрелок под цвет канала */}
{Array.from(new Set(connections.map(c => c.tunnelType))).map((t) => {
const style = getTunnelStyle(t);
@@ -188,7 +219,13 @@ function GraphView({ servers, connections }) {
+ {/* Тень карточки */}
+
+
+
+ {/* Подложка сетки */}
+
{/* Связи */}
{connections.map((connection, index) => {
@@ -229,6 +266,7 @@ function GraphView({ servers, connections }) {
markerEnd={`url(#arrow-${connection.tunnelType})`}
filter={isHovered ? 'url(#glow)' : undefined}
opacity={isHovered ? 1 : 0.9}
+ style={{ vectorEffect: 'non-scaling-stroke' }}
/>
{/* Подпись связи */}
@@ -242,7 +280,7 @@ function GraphView({ servers, connections }) {
stroke={style.color}
strokeWidth="1"
/>
-
- {/* Тень */}
-
-
- {/* Основной круг */}
- handleMouseDown(e, server.ip)}
/>
-
- {/* Внутренний круг с цветом страны */}
-
-
- {/* Иконка сервера */}
- 🖥️
-
-
- {/* DNS имя */}
-
+ {server.country}
+
+ {/* Название */}
+
{server.dns}
-
- {/* IP адрес */}
+ {/* IP */}
{server.ip}
-
{/* Провайдер */}
- {server.provider}
-
-
- {/* Бейдж страны */}
-
-
- {server.country}
+ {server.provider}
);