diff --git a/backend/server.js b/backend/server.js index a477145..17b60c3 100644 --- a/backend/server.js +++ b/backend/server.js @@ -305,6 +305,20 @@ const serversRoutes = createJsonDataRoutes('servers.json', (server, i) => { app.get('/api/servers', serversRoutes.get); app.post('/api/servers', serversRoutes.post); +// Server connections for topology graph +const serverConnectionsRoutes = createJsonDataRoutes('server-connections.json', (conn, i) => { + if (!conn.from || !conn.to || !conn.tunnelType) { + return `Connection at index ${i} is missing required fields: from, to, tunnelType`; + } + if (conn.from === conn.to) { + return `Connection at index ${i} has identical from/to values`; + } + return null; +}); + +app.get('/api/server-connections', serverConnectionsRoutes.get); +app.post('/api/server-connections', serverConnectionsRoutes.post); + // Billing const billingRoutes = createJsonDataRoutes('servers-billing.json', (item, i) => { if (!item.hostName || !item.country || !item.provider) { diff --git a/frontend/src/App.css b/frontend/src/App.css index 13a90cd..c0aea78 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -520,4 +520,40 @@ button:focus-visible, 100% { transform: scale(1); } +} + +/* ===== GraphView / ReactFlow custom styling ===== */ + +/* Плавные, аккуратные линии графа */ +.react-flow__edge-path { + stroke-linecap: round; + transition: stroke 0.15s ease, stroke-width 0.15s ease, filter 0.15s ease; +} + +/* Лёгкий ховер по связи, без агрессивной анимации */ +.react-flow__edge:hover .react-flow__edge-path { + stroke-width: 3.2px; + filter: drop-shadow(0 0 4px rgba(15, 23, 42, 0.25)); +} + +/* Базовая прозрачность для всех связей, чтобы граф не выглядел «забитым» */ +.react-flow__edge .react-flow__edge-path { + stroke-opacity: 0.9; +} + +/* Небольшие отличия по типу туннеля (на будущее, сейчас основное зашито в JS) */ +.react-flow__edge.edge-tunnel-gre .react-flow__edge-path { + /* GRE — базовый синий, уже задан в JS, здесь только подчёркиваем плавность */ +} + +.react-flow__edge.edge-tunnel-ipsec .react-flow__edge-path { + stroke-dasharray: 6 4; +} + +.react-flow__edge.edge-tunnel-wireguard .react-flow__edge-path { + stroke-width: 3px; +} + +.react-flow__edge.edge-tunnel-openvpn .react-flow__edge-path { + stroke-dasharray: 3 3; } \ No newline at end of file diff --git a/frontend/src/GraphView.jsx b/frontend/src/GraphView.jsx index 2f549fb..b50683d 100644 --- a/frontend/src/GraphView.jsx +++ b/frontend/src/GraphView.jsx @@ -77,6 +77,7 @@ function GraphView({ servers, connections, onCreateConnection }) { source: String(c.from), target: String(c.to), label: ipLabel ? `${baseLabel} · ${ipLabel}` : baseLabel, + type: 'smoothstep', style: { stroke: style.color, strokeWidth: style.width, @@ -89,7 +90,9 @@ function GraphView({ servers, connections, onCreateConnection }) { fontWeight: 500, fill: '#0f172a', }, - markerEnd: { type: MarkerType.ArrowClosed, color: style.color, width: 20, height: 20 }, + className: c.tunnelType ? `edge-tunnel-${String(c.tunnelType).toLowerCase()}` : 'edge-tunnel-default', + markerStart: { type: MarkerType.ArrowClosed, color: style.color, width: 16, height: 16 }, + markerEnd: { type: MarkerType.ArrowClosed, color: style.color, width: 16, height: 16 }, animated: false, }; }); @@ -251,8 +254,7 @@ function GraphView({ servers, connections, onCreateConnection }) { nodeTypes={nodeTypes} fitView defaultEdgeOptions={{ - type: 'default', - markerEnd: { type: MarkerType.ArrowClosed }, + type: 'smoothstep', }} onInit={onInit} style={{ width: '100%', height: '100%', background: '#f8fafc' }} @@ -267,46 +269,54 @@ function GraphView({ servers, connections, onCreateConnection }) { {/* Своя панель управления: -, +, Fit, Fullscreen */}
- - - - + + + + + + + + + + + +
diff --git a/frontend/src/ServerManager.jsx b/frontend/src/ServerManager.jsx index 8f1c435..a65b454 100644 --- a/frontend/src/ServerManager.jsx +++ b/frontend/src/ServerManager.jsx @@ -90,10 +90,7 @@ function ServerManager() { const [addServerModalOpen, setAddServerModalOpen] = useState(false); const [customProvider, setCustomProvider] = useState(''); - const [connections, setConnections] = useState([ - // Пример связи для теста - // { from: '192.168.1.1', to: '192.168.1.2', tunnelType: 'GRE', ipA: '10.10.100.1', ipB: '10.10.100.2' } - ]); + const [connections, setConnections] = useState([]); const [newConnection, setNewConnection] = useState({ from: '', to: '', tunnelType: 'GRE', ipA: '', ipB: '' }); const [graphConnectionModalOpen, setGraphConnectionModalOpen] = useState(false); @@ -105,6 +102,7 @@ function ServerManager() { useEffect(() => { fetchServers(); + fetchConnections(); }, []); // Загружаем настройки URL из localStorage @@ -134,6 +132,20 @@ function ServerManager() { } }; + const fetchConnections = async () => { + try { + const response = await api.get(`/server-connections`); + if (Array.isArray(response.data)) { + setConnections(response.data); + } else { + setConnections([]); + } + } catch (error) { + console.error('Error fetching server connections:', error); + // Не перекрываем возможную ошибку по серверам, просто логируем + } + }; + const handleAddServer = () => { if (!newServer.ip.trim() || !newServer.dns.trim() || !newServer.country.trim() || !newServer.provider.trim() || !newServer.tunnel.trim()) { setError('Все обязательные поля должны быть заполнены.'); @@ -374,11 +386,12 @@ function ServerManager() { setLoading(true); try { await api.post(`/servers`, { domains: servers }); - setSuccess('Изменения успешно сохранены!'); + await api.post(`/server-connections`, { domains: connections }); + setSuccess('Изменения по серверам и связям успешно сохранены!'); setTimeout(() => setSuccess(''), 3000); } catch (error) { console.error('Error saving changes:', error); - setError('Не удалось сохранить изменения.'); + setError('Не удалось сохранить изменения по серверам или связям.'); } finally { setLoading(false); }