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