diff --git a/frontend/src/GraphView.jsx b/frontend/src/GraphView.jsx index 1194563..2f549fb 100644 --- a/frontend/src/GraphView.jsx +++ b/frontend/src/GraphView.jsx @@ -12,12 +12,14 @@ import { } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { IconZoomIn, IconZoomOut, IconMaximize, IconMinimize } from '@tabler/icons-react'; +import Tooltip from './components/Tooltip.jsx'; function GraphView({ servers, connections, onCreateConnection }) { const flowRef = useRef(null); const instanceRef = useRef(null); const containerRef = useRef(null); const [isFullscreen, setIsFullscreen] = useState(false); + const [highlightedNodeId, setHighlightedNodeId] = useState(null); const getTunnelStyle = useCallback((tunnelType) => { const map = { @@ -68,12 +70,25 @@ function GraphView({ servers, connections, onCreateConnection }) { const initialEdgesData = useMemo(() => { return (connections || []).map((c, idx) => { const style = getTunnelStyle(c.tunnelType); + const baseLabel = c.tunnelType || 'TUNNEL'; + const ipLabel = c.ipA && c.ipB ? `${c.ipA} ⇄ ${c.ipB}` : ''; return { id: `${c.from}-${c.to}-${idx}`, source: String(c.from), target: String(c.to), - label: c.ipA && c.ipB ? `${c.tunnelType} ${c.ipA} ⇄ ${c.ipB}` : c.tunnelType, - style: { stroke: style.color, strokeWidth: style.width, strokeDasharray: style.dash }, + label: ipLabel ? `${baseLabel} · ${ipLabel}` : baseLabel, + style: { + stroke: style.color, + strokeWidth: style.width, + strokeDasharray: style.dash, + }, + labelBgPadding: [6, 4], + labelBgBorderRadius: 999, + labelStyle: { + fontSize: 11, + fontWeight: 500, + fill: '#0f172a', + }, markerEnd: { type: MarkerType.ArrowClosed, color: style.color, width: 20, height: 20 }, animated: false, }; @@ -99,7 +114,7 @@ function GraphView({ servers, connections, onCreateConnection }) { const nodeTypes = useMemo( () => ({ - server: ({ data }) => { + server: ({ id, data }) => { const s = data.server || {}; const countryColor = getCountryColor(s.country); const getFlagEmoji = (code) => { @@ -111,16 +126,24 @@ function GraphView({ servers, connections, onCreateConnection }) { return cc.replace(/./g, (ch) => String.fromCodePoint(127397 + ch.charCodeAt())); }; const flag = getFlagEmoji(s.country); + const isHighlighted = highlightedNodeId === id; return (
setHighlightedNodeId(id)} + onMouseLeave={() => setHighlightedNodeId((prev) => (prev === id ? null : prev))} > {/* Точка входа соединений */} diff --git a/frontend/src/ServerManager.jsx b/frontend/src/ServerManager.jsx index 629cc1b..8f1c435 100644 --- a/frontend/src/ServerManager.jsx +++ b/frontend/src/ServerManager.jsx @@ -95,6 +95,7 @@ function ServerManager() { // { from: '192.168.1.1', to: '192.168.1.2', tunnelType: 'GRE', ipA: '10.10.100.1', ipB: '10.10.100.2' } ]); const [newConnection, setNewConnection] = useState({ from: '', to: '', tunnelType: 'GRE', ipA: '', ipB: '' }); + const [graphConnectionModalOpen, setGraphConnectionModalOpen] = useState(false); // Фильтры графа const [graphTunnelFilters, setGraphTunnelFilters] = useState({ GRE: true, IPSec: true, WireGuard: true, OpenVPN: true }); @@ -475,21 +476,38 @@ function ServerManager() { ); } - // Добавление новой связи - const handleAddConnection = () => { - if (!newConnection.from || !newConnection.to || !newConnection.tunnelType || !newConnection.ipA || !newConnection.ipB) { + // Валидация и добавление связи (общая функция для формы и модального окна графа) + const addConnectionInternal = (draft) => { + if (!draft.from || !draft.to || !draft.tunnelType || !draft.ipA || !draft.ipB) { setError('Все поля для связи должны быть заполнены.'); - return; + return false; } - if (newConnection.from === newConnection.to) { + if (draft.from === draft.to) { setError('Сервер A и сервер B не могут быть одинаковыми.'); - return; + return false; } - setConnections([...connections, newConnection]); - setNewConnection({ from: '', to: '', tunnelType: 'GRE', ipA: '', ipB: '' }); - setError(''); + const next = { ...draft }; + setConnections((prev) => [...prev, next]); setSuccess('Связь успешно добавлена!'); setTimeout(() => setSuccess(''), 3000); + return true; + }; + + // Добавление новой связи из нижней формы + const handleAddConnection = () => { + const ok = addConnectionInternal(newConnection); + if (!ok) return; + setNewConnection({ from: '', to: '', tunnelType: 'GRE', ipA: '', ipB: '' }); + setError(''); + }; + + // Подтверждение связи, созданной через граф (модальное окно) + const handleConfirmGraphConnection = () => { + const ok = addConnectionInternal(newConnection); + if (!ok) return; + setGraphConnectionModalOpen(false); + setNewConnection({ from: '', to: '', tunnelType: 'GRE', ipA: '', ipB: '' }); + setError(''); }; // Удаление связи @@ -961,10 +979,19 @@ function ServerManager() { servers={servers.filter(s => (!graphCountryFilter || s.country === graphCountryFilter) && (!graphProviderFilter || s.provider === graphProviderFilter))} connections={connections.filter(c => graphTunnelFilters[c.tunnelType])} onCreateConnection={({ from, to }) => { - // Открываем модал добавления связи и проставляем выбранные узлы - setNewConnection(prev => ({ ...prev, from, to })); - const el = document.querySelector('#add-connection-anchor'); - if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' }); + // Открываем современный флоу создания связи прямо поверх графа + setNewConnection(prev => ({ + ...prev, + from, + to, + // Если серверы уже имеют тип туннеля, предлагаем его по умолчанию + tunnelType: + prev.tunnelType || + servers.find(s => s.ip === from)?.tunnel || + servers.find(s => s.ip === to)?.tunnel || + 'GRE', + })); + setGraphConnectionModalOpen(true); }} />
@@ -977,6 +1004,80 @@ function ServerManager() {
+ {/* Модальное окно для быстрого создания связи после соединения узлов на графе */} + { + setGraphConnectionModalOpen(false); + setNewConnection({ from: '', to: '', tunnelType: 'GRE', ipA: '', ipB: '' }); + setError(''); + }} + onSubmit={handleConfirmGraphConnection} + title="Создать связь между серверами" + submitLabel="Создать связь" + submitIcon={IconPlus} + size="md" + > + {error && ( +
+ {error} +
+ )} +
+
Вы выбрали связь между:
+
+ + {newConnection.from || 'Сервер A'} + + + + {newConnection.to || 'Сервер B'} + +
+
+
+
+ setNewConnection({ ...newConnection, tunnelType: val })} + required + options={[ + { value: 'GRE', label: 'GRE' }, + { value: 'IPSec', label: 'IPSec' }, + { value: 'WireGuard', label: 'WireGuard' }, + { value: 'OpenVPN', label: 'OpenVPN' }, + ]} + /> +
+
+ setNewConnection({ ...newConnection, ipA: val })} + placeholder="10.10.100.1" + required + /> +
+
+ setNewConnection({ ...newConnection, ipB: val })} + placeholder="10.10.100.2" + required + /> +
+
+
+ Протяните линию между серверами в графе — здесь вы только уточняете параметры туннеля. Это делает + построение схемы максимально быстрым и наглядным. +
+

Добавить связь между серверами