feat: Enhance GraphView component with draggable nodes, improved positioning, and country color coding for better visualization of server connections
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 7m24s

This commit is contained in:
2025-07-15 23:46:14 +07:00
parent bbf143dee7
commit 04766bd005
+190 -37
View File
@@ -1,6 +1,11 @@
import React from 'react'; import React, { useState, useRef } from 'react';
function GraphView({ servers, connections }) { function GraphView({ servers, connections }) {
const [nodePositions, setNodePositions] = useState({});
const [draggedNode, setDraggedNode] = useState(null);
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
const svgRef = useRef(null);
if (servers.length === 0) { if (servers.length === 0) {
return ( return (
<div className="text-center py-5"> <div className="text-center py-5">
@@ -12,21 +17,99 @@ function GraphView({ servers, connections }) {
); );
} }
// Простое позиционирование узлов в сетке // Инициализация позиций узлов при первом рендере
const getNodePosition = (index, total) => { const getInitialNodePosition = (index, total) => {
if (nodePositions[servers[index]?.ip]) {
return nodePositions[servers[index].ip];
}
const cols = Math.ceil(Math.sqrt(total)); const cols = Math.ceil(Math.sqrt(total));
const row = Math.floor(index / cols); const row = Math.floor(index / cols);
const col = index % cols; const col = index % cols;
const spacing = 150; const spacing = 180;
return { return {
x: 100 + col * spacing, x: 120 + col * spacing,
y: 100 + row * spacing y: 120 + row * spacing
}; };
}; };
// Обработчик начала перетаскивания
const handleMouseDown = (e, serverIp) => {
const svg = svgRef.current;
const pt = svg.createSVGPoint();
pt.x = e.clientX;
pt.y = e.clientY;
const svgP = pt.matrixTransform(svg.getScreenCTM().inverse());
const currentPos = nodePositions[serverIp] || getInitialNodePosition(
servers.findIndex(s => s.ip === serverIp),
servers.length
);
setDragOffset({
x: svgP.x - currentPos.x,
y: svgP.y - currentPos.y
});
setDraggedNode(serverIp);
};
// Обработчик перетаскивания
const handleMouseMove = (e) => {
if (!draggedNode) return;
const svg = svgRef.current;
const pt = svg.createSVGPoint();
pt.x = e.clientX;
pt.y = e.clientY;
const svgP = pt.matrixTransform(svg.getScreenCTM().inverse());
setNodePositions(prev => ({
...prev,
[draggedNode]: {
x: svgP.x - dragOffset.x,
y: svgP.y - dragOffset.y
}
}));
};
// Обработчик окончания перетаскивания
const handleMouseUp = () => {
setDraggedNode(null);
};
// Получение позиции узла
const getNodePosition = (serverIp) => {
return nodePositions[serverIp] || getInitialNodePosition(
servers.findIndex(s => s.ip === serverIp),
servers.length
);
};
// Цвета для разных стран
const getCountryColor = (country) => {
const colors = {
'RU': '#dc2626', // красный
'US': '#2563eb', // синий
'DE': '#059669', // зеленый
'SE': '#7c3aed', // фиолетовый
'NL': '#ea580c', // оранжевый
'SG': '#0891b2' // голубой
};
return colors[country] || '#6b7280';
};
return ( return (
<div style={{ width: '100%', height: '500px', border: '1px solid #e5e7eb', borderRadius: '8px', overflow: 'hidden', background: '#f8fafc' }}> <div style={{ width: '100%', height: '500px', border: '1px solid #e5e7eb', borderRadius: '8px', overflow: 'hidden', background: '#f8fafc' }}>
<svg width="100%" height="100%" viewBox="0 0 800 500"> <svg
ref={svgRef}
width="100%"
height="100%"
viewBox="0 0 900 600"
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
style={{ cursor: draggedNode ? 'grabbing' : 'default' }}
>
{/* Связи */} {/* Связи */}
{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);
@@ -34,10 +117,8 @@ function GraphView({ servers, connections }) {
if (!fromServer || !toServer) return null; if (!fromServer || !toServer) return null;
const fromIndex = servers.indexOf(fromServer); const fromPos = getNodePosition(connection.from);
const toIndex = servers.indexOf(toServer); const toPos = getNodePosition(connection.to);
const fromPos = getNodePosition(fromIndex, servers.length);
const toPos = getNodePosition(toIndex, servers.length);
return ( return (
<g key={`link-${index}`}> <g key={`link-${index}`}>
@@ -47,17 +128,28 @@ function GraphView({ servers, connections }) {
y1={fromPos.y} y1={fromPos.y}
x2={toPos.x} x2={toPos.x}
y2={toPos.y} y2={toPos.y}
stroke="#64748b" stroke="#94a3b8"
strokeWidth="2" strokeWidth="3"
strokeDasharray="5,5"
markerEnd="url(#arrowhead)" markerEnd="url(#arrowhead)"
/> />
{/* Подпись связи */} {/* Подпись связи */}
<rect
x={(fromPos.x + toPos.x) / 2 - 40}
y={(fromPos.y + toPos.y) / 2 - 15}
width="80"
height="30"
rx="15"
fill="white"
stroke="#e5e7eb"
strokeWidth="1"
/>
<text <text
x={(fromPos.x + toPos.x) / 2} x={(fromPos.x + toPos.x) / 2}
y={(fromPos.y + toPos.y) / 2 - 10} y={(fromPos.y + toPos.y) / 2}
textAnchor="middle" textAnchor="middle"
fontSize="12" fontSize="11"
fill="#374151" fill="#374151"
fontWeight="bold" fontWeight="bold"
> >
@@ -65,9 +157,9 @@ function GraphView({ servers, connections }) {
</text> </text>
<text <text
x={(fromPos.x + toPos.x) / 2} x={(fromPos.x + toPos.x) / 2}
y={(fromPos.y + toPos.y) / 2 + 5} y={(fromPos.y + toPos.y) / 2 + 15}
textAnchor="middle" textAnchor="middle"
fontSize="10" fontSize="9"
fill="#6b7280" fill="#6b7280"
> >
{connection.ipA} ⇄ {connection.ipB} {connection.ipA} ⇄ {connection.ipB}
@@ -77,29 +169,65 @@ function GraphView({ servers, connections }) {
})} })}
{/* Узлы (серверы) */} {/* Узлы (серверы) */}
{servers.map((server, index) => { {servers.map((server) => {
const pos = getNodePosition(index, servers.length); const pos = getNodePosition(server.ip);
const countryColor = getCountryColor(server.country);
const isDragging = draggedNode === server.ip;
return ( return (
<g key={server.ip}> <g key={server.ip}>
{/* Круг узла */} {/* Тень */}
<circle
cx={pos.x + 2}
cy={pos.y + 2}
r="45"
fill="rgba(0,0,0,0.1)"
style={{ pointerEvents: 'none' }}
/>
{/* Основной круг */}
<circle <circle
cx={pos.x} cx={pos.x}
cy={pos.y} cy={pos.y}
r="30" r="45"
fill="#3b82f6" fill={isDragging ? "#f1f5f9" : "white"}
stroke="#1e40af" stroke={countryColor}
strokeWidth="2" strokeWidth="3"
style={{ cursor: 'grab' }}
onMouseDown={(e) => handleMouseDown(e, server.ip)}
/> />
{/* DNS имя */} {/* Внутренний круг с цветом страны */}
<circle
cx={pos.x}
cy={pos.y}
r="35"
fill={countryColor}
style={{ pointerEvents: 'none' }}
/>
{/* Иконка сервера */}
<text <text
x={pos.x} x={pos.x}
y={pos.y - 8} y={pos.y - 8}
textAnchor="middle" textAnchor="middle"
fontSize="11" fontSize="16"
fill="white" fill="white"
fontWeight="bold" fontWeight="bold"
style={{ pointerEvents: 'none' }}
>
🖥️
</text>
{/* DNS имя */}
<text
x={pos.x}
y={pos.y + 15}
textAnchor="middle"
fontSize="10"
fill="white"
fontWeight="bold"
style={{ pointerEvents: 'none' }}
> >
{server.dns} {server.dns}
</text> </text>
@@ -107,21 +235,46 @@ function GraphView({ servers, connections }) {
{/* IP адрес */} {/* IP адрес */}
<text <text
x={pos.x} x={pos.x}
y={pos.y + 8} y={pos.y + 28}
textAnchor="middle" textAnchor="middle"
fontSize="10" fontSize="9"
fill="white" fill="white"
style={{ pointerEvents: 'none' }}
> >
{server.ip} {server.ip}
</text> </text>
{/* Страна */} {/* Провайдер */}
<text <text
x={pos.x} x={pos.x}
y={pos.y + 25} y={pos.y + 45}
textAnchor="middle" textAnchor="middle"
fontSize="9" fontSize="8"
fill="#6b7280" fill="#6b7280"
fontWeight="bold"
style={{ pointerEvents: 'none' }}
>
{server.provider}
</text>
{/* Бейдж страны */}
<rect
x={pos.x - 20}
y={pos.y - 55}
width="40"
height="20"
rx="10"
fill={countryColor}
style={{ pointerEvents: 'none' }}
/>
<text
x={pos.x}
y={pos.y - 42}
textAnchor="middle"
fontSize="10"
fill="white"
fontWeight="bold"
style={{ pointerEvents: 'none' }}
> >
{server.country} {server.country}
</text> </text>
@@ -133,15 +286,15 @@ function GraphView({ servers, connections }) {
<defs> <defs>
<marker <marker
id="arrowhead" id="arrowhead"
markerWidth="10" markerWidth="12"
markerHeight="7" markerHeight="8"
refX="9" refX="10"
refY="3.5" refY="4"
orient="auto" orient="auto"
> >
<polygon <polygon
points="0 0, 10 3.5, 0 7" points="0 0, 12 4, 0 8"
fill="#64748b" fill="#94a3b8"
/> />
</marker> </marker>
</defs> </defs>