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 }) {
|
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user