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 }) {
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) {
return (
<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 row = Math.floor(index / cols);
const col = index % cols;
const spacing = 150;
const spacing = 180;
return {
x: 100 + col * spacing,
y: 100 + row * spacing
x: 120 + col * 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 (
<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) => {
const fromServer = servers.find(s => s.ip === connection.from);
@@ -34,10 +117,8 @@ function GraphView({ servers, connections }) {
if (!fromServer || !toServer) return null;
const fromIndex = servers.indexOf(fromServer);
const toIndex = servers.indexOf(toServer);
const fromPos = getNodePosition(fromIndex, servers.length);
const toPos = getNodePosition(toIndex, servers.length);
const fromPos = getNodePosition(connection.from);
const toPos = getNodePosition(connection.to);
return (
<g key={`link-${index}`}>
@@ -47,17 +128,28 @@ function GraphView({ servers, connections }) {
y1={fromPos.y}
x2={toPos.x}
y2={toPos.y}
stroke="#64748b"
strokeWidth="2"
stroke="#94a3b8"
strokeWidth="3"
strokeDasharray="5,5"
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
x={(fromPos.x + toPos.x) / 2}
y={(fromPos.y + toPos.y) / 2 - 10}
y={(fromPos.y + toPos.y) / 2}
textAnchor="middle"
fontSize="12"
fontSize="11"
fill="#374151"
fontWeight="bold"
>
@@ -65,9 +157,9 @@ function GraphView({ servers, connections }) {
</text>
<text
x={(fromPos.x + toPos.x) / 2}
y={(fromPos.y + toPos.y) / 2 + 5}
y={(fromPos.y + toPos.y) / 2 + 15}
textAnchor="middle"
fontSize="10"
fontSize="9"
fill="#6b7280"
>
{connection.ipA} {connection.ipB}
@@ -77,29 +169,65 @@ function GraphView({ servers, connections }) {
})}
{/* Узлы (серверы) */}
{servers.map((server, index) => {
const pos = getNodePosition(index, servers.length);
{servers.map((server) => {
const pos = getNodePosition(server.ip);
const countryColor = getCountryColor(server.country);
const isDragging = draggedNode === server.ip;
return (
<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
cx={pos.x}
cy={pos.y}
r="30"
fill="#3b82f6"
stroke="#1e40af"
strokeWidth="2"
r="45"
fill={isDragging ? "#f1f5f9" : "white"}
stroke={countryColor}
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
x={pos.x}
y={pos.y - 8}
textAnchor="middle"
fontSize="11"
fontSize="16"
fill="white"
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}
</text>
@@ -107,21 +235,46 @@ function GraphView({ servers, connections }) {
{/* IP адрес */}
<text
x={pos.x}
y={pos.y + 8}
y={pos.y + 28}
textAnchor="middle"
fontSize="10"
fontSize="9"
fill="white"
style={{ pointerEvents: 'none' }}
>
{server.ip}
</text>
{/* Страна */}
{/* Провайдер */}
<text
x={pos.x}
y={pos.y + 25}
y={pos.y + 45}
textAnchor="middle"
fontSize="9"
fontSize="8"
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}
</text>
@@ -133,15 +286,15 @@ function GraphView({ servers, connections }) {
<defs>
<marker
id="arrowhead"
markerWidth="10"
markerHeight="7"
refX="9"
refY="3.5"
markerWidth="12"
markerHeight="8"
refX="10"
refY="4"
orient="auto"
>
<polygon
points="0 0, 10 3.5, 0 7"
fill="#64748b"
points="0 0, 12 4, 0 8"
fill="#94a3b8"
/>
</marker>
</defs>