import { useState, useEffect, useMemo } from 'react'; import api from './lib/api.js'; import { useNotify } from './components/NotifyProvider.jsx'; import FormModal from './components/FormModal.jsx'; import FormField from './components/FormField.jsx'; import ConfirmModal from './components/ConfirmModal.jsx'; import ServerAutocompleteInput from './components/ServerAutocompleteInput.jsx'; import GatewayAutocompleteInput from './components/GatewayAutocompleteInput.jsx'; import { countryToFlag } from './utils/serverUtils.js'; import { IconNetwork, IconPlus, IconEdit, IconTrash, IconDatabase, IconServer, IconWorld, IconRouter, IconCopy, IconSearch, IconRefresh, IconFilter, IconX, IconCircleFilled, IconLayoutGrid, IconList, IconCode, IconArrowsRightLeft } from '@tabler/icons-react'; /** * Менеджер сетевых настроек * Справочник IP-адресов, интерфейсов и gateway для серверов */ // Типы провайдеров gateway const GATEWAY_PROVIDERS = [ { value: 'cloudflare', label: 'Cloudflare', color: 'orange' }, { value: 'bunny', label: 'Bunny CDN', color: 'yellow' }, { value: 'fastly', label: 'Fastly', color: 'red' }, { value: 'telegram', label: 'Telegram', color: 'blue' }, { value: 'hetzner', label: 'Hetzner', color: 'green' }, { value: 'yandex', label: 'Yandex Cloud', color: 'cyan' }, { value: 'custom', label: 'Другой', color: 'secondary' }, ]; // Типы туннельных интерфейсов const INTERFACE_TYPES = [ { value: 'GRE', label: 'GRE', color: 'blue' }, { value: 'WireGuard', label: 'WireGuard', color: 'green' }, { value: 'IPSec', label: 'IPSec', color: 'yellow' }, { value: 'VXLAN', label: 'VXLAN', color: 'purple' }, { value: 'OpenVPN', label: 'OpenVPN', color: 'orange' }, ]; // Типы gateway const GATEWAY_TYPES = [ { value: 'direct', label: 'Прямой', description: 'Прямой доступ в интернет', color: 'green' }, { value: 'recursive', label: 'Рекурсивный', description: 'Ссылается на другой gateway', color: 'blue' }, ]; // Пустые объекты const getEmptyGateway = () => ({ id: `gw-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`, ip: '', description: '', serverId: '', country: '', type: 'direct', parentGatewayId: '', // Для рекурсивных - ID родительского gateway }); const getEmptyInterface = () => ({ id: `if-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`, name: '', name2: '', // Имя интерфейса на втором сервере (опционально) type: 'GRE', localIp: '', remoteIp: '', serverId: '', serverId2: '', // Второй сервер (опциональный) }); const getEmptyIpPool = () => ({ id: `pool-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`, name: '', cidr: '', description: '', }); const getDefaultConfig = () => ({ gateways: [], tunnelInterfaces: [], ipPools: [], }); function NetworkConfigManager() { const notify = useNotify(); // === Основные данные === const [config, setConfig] = useState(getDefaultConfig()); const [servers, setServers] = useState([]); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); // === UI State === const [activeTab, setActiveTab] = useState('gateways'); const [searchTerm, setSearchTerm] = useState(''); const [providerFilter, setProviderFilter] = useState(''); const [serverFilter, setServerFilter] = useState(''); const [typeFilter, setTypeFilter] = useState(''); const [gatewayTypeFilter, setGatewayTypeFilter] = useState(''); // Для фильтрации gateway по типу const [viewMode, setViewMode] = useState('cards'); // 'cards' | 'table' // === Modals === const [gatewayModalOpen, setGatewayModalOpen] = useState(false); const [gatewayModalMode, setGatewayModalMode] = useState('add'); const [editingGateway, setEditingGateway] = useState(null); const [interfaceModalOpen, setInterfaceModalOpen] = useState(false); const [interfaceModalMode, setInterfaceModalMode] = useState('add'); const [editingInterface, setEditingInterface] = useState(null); const [poolModalOpen, setPoolModalOpen] = useState(false); const [poolModalMode, setPoolModalMode] = useState('add'); const [editingPool, setEditingPool] = useState(null); const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [itemToDelete, setItemToDelete] = useState(null); const [deleteType, setDeleteType] = useState(''); // === MikroTik Code Generation === const [mikrotikCodeModalOpen, setMikrotikCodeModalOpen] = useState(false); const [generatedMikrotikCode, setGeneratedMikrotikCode] = useState([]); // === Загрузка данных === useEffect(() => { fetchConfig(); fetchServers(); }, []); const fetchConfig = async () => { setLoading(true); try { const response = await api.get('/network-config'); const data = response.data || getDefaultConfig(); setConfig({ gateways: Array.isArray(data.gateways) ? data.gateways : [], tunnelInterfaces: Array.isArray(data.tunnelInterfaces) ? data.tunnelInterfaces : [], ipPools: Array.isArray(data.ipPools) ? data.ipPools : [], }); } catch (error) { if (error.response?.status !== 404) { console.error('Error fetching network config:', error); notify.error('Не удалось загрузить сетевые настройки'); } setConfig(getDefaultConfig()); } finally { setLoading(false); } }; const fetchServers = async () => { try { const response = await api.get('/servers'); setServers(Array.isArray(response.data) ? response.data : []); } catch (error) { console.error('Error fetching servers:', error); } }; // === Сохранение === const handleSave = async () => { setSaving(true); try { await api.post('/network-config', { domains: config }); notify.success('Сетевые настройки сохранены'); } catch (error) { console.error('Error saving network config:', error); notify.error('Не удалось сохранить настройки'); } finally { setSaving(false); } }; // === Получение информации о сервере === const getServerInfo = (serverId) => { if (!serverId) return null; return servers.find(s => s.id === serverId || s.ip === serverId || s.dns === serverId); }; const getServerLabel = (serverId) => { const server = getServerInfo(serverId); if (!server) return serverId || '—'; return server.dns || server.ip; }; // === Уникальные серверы для фильтра === const uniqueServersInConfig = useMemo(() => { const allServerIds = new Set([ ...(config.gateways || []).map(g => g.serverId).filter(Boolean), ...(config.tunnelInterfaces || []).map(i => i.serverId).filter(Boolean), ]); return Array.from(allServerIds).map(id => ({ id, label: getServerLabel(id), server: getServerInfo(id), })); }, [config.gateways, config.tunnelInterfaces, servers]); // === Фильтрация === const filteredGateways = useMemo(() => { let result = [...(config.gateways || [])]; if (providerFilter) { // Фильтруем по провайдеру сервера result = result.filter(g => { const server = getServerInfo(g.serverId); return server?.provider === providerFilter; }); } if (serverFilter) { result = result.filter(g => g.serverId === serverFilter); } if (gatewayTypeFilter) { result = result.filter(g => g.type === gatewayTypeFilter); } if (searchTerm) { const term = searchTerm.toLowerCase(); result = result.filter(g => { const server = getServerInfo(g.serverId); return ( g.ip?.toLowerCase().includes(term) || g.country?.toLowerCase().includes(term) || server?.country?.toLowerCase().includes(term) || server?.provider?.toLowerCase().includes(term) || g.description?.toLowerCase().includes(term) || getServerLabel(g.serverId).toLowerCase().includes(term) ); }); } return result; }, [config.gateways, providerFilter, serverFilter, typeFilter, searchTerm, servers]); const filteredInterfaces = useMemo(() => { let result = [...(config.tunnelInterfaces || [])]; if (typeFilter) { result = result.filter(i => i.type === typeFilter); } if (serverFilter) { result = result.filter(i => i.serverId === serverFilter); } if (searchTerm) { const term = searchTerm.toLowerCase(); result = result.filter(i => i.name?.toLowerCase().includes(term) || i.localIp?.toLowerCase().includes(term) || i.remoteIp?.toLowerCase().includes(term) || i.type?.toLowerCase().includes(term) || getServerLabel(i.serverId).toLowerCase().includes(term) ); } return result; }, [config.tunnelInterfaces, typeFilter, serverFilter, searchTerm, servers]); const filteredPools = useMemo(() => { let result = [...(config.ipPools || [])]; if (searchTerm) { const term = searchTerm.toLowerCase(); result = result.filter(p => p.name?.toLowerCase().includes(term) || p.cidr?.toLowerCase().includes(term) || p.description?.toLowerCase().includes(term) ); } return result; }, [config.ipPools, searchTerm]); // === Группировка по серверам === const gatewaysByServer = useMemo(() => { const grouped = {}; filteredGateways.forEach(gw => { const key = gw.serverId || '__unassigned__'; if (!grouped[key]) grouped[key] = []; grouped[key].push(gw); }); return grouped; }, [filteredGateways]); const interfacesByServer = useMemo(() => { const grouped = {}; filteredInterfaces.forEach(iface => { // Группируем по первому серверу const key1 = iface.serverId || '__unassigned__'; if (!grouped[key1]) grouped[key1] = []; grouped[key1].push(iface); // Если есть второй сервер, также добавляем в его группу if (iface.serverId2) { const key2 = iface.serverId2; if (!grouped[key2]) grouped[key2] = []; grouped[key2].push(iface); } }); return grouped; }, [filteredInterfaces]); // === Сброс фильтров === const resetFilters = () => { setSearchTerm(''); setProviderFilter(''); setServerFilter(''); setTypeFilter(''); setGatewayTypeFilter(''); }; const hasActiveFilters = searchTerm || providerFilter || serverFilter || typeFilter || gatewayTypeFilter; // === CRUD для Gateways === const handleAddGateway = () => { setEditingGateway(getEmptyGateway()); setGatewayModalMode('add'); setGatewayModalOpen(true); }; const handleEditGateway = (gateway) => { setEditingGateway({ ...gateway }); setGatewayModalMode('edit'); setGatewayModalOpen(true); }; const handleSaveGateway = (gatewayData) => { // Валидация: IP адрес обязателен if (!gatewayData.ip || !gatewayData.ip.trim()) { notify.error('IP адрес обязателен'); return; } if (gatewayModalMode === 'add') { // Проверка на дубликат IP const existingGateway = config.gateways.find(g => g.ip === gatewayData.ip.trim()); if (existingGateway) { notify.error(`Gateway с IP ${gatewayData.ip} уже существует`); return; } setConfig(prev => ({ ...prev, gateways: [...prev.gateways, gatewayData], })); notify.success('Gateway добавлен'); } else { // При редактировании ищем по ID, но проверяем, не занят ли новый IP другим gateway const currentGateway = config.gateways.find(g => g.id === gatewayData.id); if (!currentGateway) { notify.error('Gateway не найден'); return; } // Если IP изменился, проверяем, не занят ли он другим gateway if (gatewayData.ip.trim() !== currentGateway.ip && config.gateways.some(g => g.id !== gatewayData.id && g.ip === gatewayData.ip.trim())) { notify.error(`Gateway с IP ${gatewayData.ip} уже существует`); return; } setConfig(prev => ({ ...prev, gateways: prev.gateways.map(g => g.id === gatewayData.id ? gatewayData : g), })); notify.success('Gateway обновлён'); } setGatewayModalOpen(false); setEditingGateway(null); }; const handleDeleteGateway = (gateway) => { setItemToDelete(gateway); setDeleteType('gateway'); setDeleteModalOpen(true); }; // === CRUD для Interfaces === const handleAddInterface = () => { setEditingInterface(getEmptyInterface()); setInterfaceModalMode('add'); setInterfaceModalOpen(true); }; const handleEditInterface = (iface) => { setEditingInterface({ ...iface }); setInterfaceModalMode('edit'); setInterfaceModalOpen(true); }; const handleSaveInterface = (ifaceData) => { if (interfaceModalMode === 'add') { setConfig(prev => ({ ...prev, tunnelInterfaces: [...prev.tunnelInterfaces, ifaceData], })); notify.success('Интерфейс добавлен'); } else { setConfig(prev => ({ ...prev, tunnelInterfaces: prev.tunnelInterfaces.map(i => i.id === ifaceData.id ? ifaceData : i), })); notify.success('Интерфейс обновлён'); } setInterfaceModalOpen(false); setEditingInterface(null); }; const handleDeleteInterface = (iface) => { setItemToDelete(iface); setDeleteType('interface'); setDeleteModalOpen(true); }; // === CRUD для IP Pools === const handleAddPool = () => { setEditingPool(getEmptyIpPool()); setPoolModalMode('add'); setPoolModalOpen(true); }; const handleEditPool = (pool) => { setEditingPool({ ...pool }); setPoolModalMode('edit'); setPoolModalOpen(true); }; const handleSavePool = (poolData) => { if (poolModalMode === 'add') { setConfig(prev => ({ ...prev, ipPools: [...prev.ipPools, poolData], })); notify.success('IP пул добавлен'); } else { setConfig(prev => ({ ...prev, ipPools: prev.ipPools.map(p => p.id === poolData.id ? poolData : p), })); notify.success('IP пул обновлён'); } setPoolModalOpen(false); setEditingPool(null); }; const handleDeletePool = (pool) => { setItemToDelete(pool); setDeleteType('pool'); setDeleteModalOpen(true); }; // === Подтверждение удаления === const executeDelete = () => { if (!itemToDelete) return; if (deleteType === 'gateway') { setConfig(prev => ({ ...prev, gateways: prev.gateways.filter(g => g.id !== itemToDelete.id), })); notify.success('Gateway удалён'); } else if (deleteType === 'interface') { setConfig(prev => ({ ...prev, tunnelInterfaces: prev.tunnelInterfaces.filter(i => i.id !== itemToDelete.id), })); notify.success('Интерфейс удалён'); } else if (deleteType === 'pool') { setConfig(prev => ({ ...prev, ipPools: prev.ipPools.filter(p => p.id !== itemToDelete.id), })); notify.success('IP пул удалён'); } setDeleteModalOpen(false); setItemToDelete(null); setDeleteType(''); }; // === Copy to clipboard === const copyToClipboard = async (text) => { try { await navigator.clipboard.writeText(text); notify.success('Скопировано'); } catch { notify.error('Не удалось скопировать'); } }; // === Генерация кода MikroTik для настройки IP адресов интерфейсов === const generateMikrotikInterfaceAddresses = () => { const interfacesWithBothServers = (config.tunnelInterfaces || []).filter(i => i.serverId && i.serverId2 && i.localIp && i.remoteIp ); if (interfacesWithBothServers.length === 0) { return []; } // Группируем по серверам const interfacesByServer = {}; interfacesWithBothServers.forEach(iface => { const server1 = getServerInfo(iface.serverId); const server2 = getServerInfo(iface.serverId2); if (!server1 || !server2) return; const server1Name = server1.dns || server1.ip || iface.serverId; const server2Name = server2.dns || server2.ip || iface.serverId2; // Имя интерфейса для первого сервера const interfaceName1 = iface.name || `${iface.type}-tunnel`; // Имя интерфейса для второго сервера - ВСЕГДА используем name2 если указано, иначе name const interfaceName2 = iface.name2 || iface.name || `${iface.type}-tunnel`; // Для первого сервера - используем localIp как local, remoteIp как remote if (!interfacesByServer[server1Name]) { interfacesByServer[server1Name] = { serverInfo: server1, interfaces: [] }; } interfacesByServer[server1Name].interfaces.push({ interfaceName: interfaceName1, localIp: iface.localIp, remoteIp: iface.remoteIp, type: iface.type, server2Name, server2Info: server2 }); // Для второго сервера - МЕНЯЕМ МЕСТАМИ: remoteIp становится localIp, localIp становится remoteIp // И ВСЕГДА используем name2 (или name если name2 не указано) if (!interfacesByServer[server2Name]) { interfacesByServer[server2Name] = { serverInfo: server2, interfaces: [] }; } interfacesByServer[server2Name].interfaces.push({ interfaceName: interfaceName2, // Используем name2 для второго сервера localIp: iface.remoteIp, // На втором сервере remoteIp первого становится localIp remoteIp: iface.localIp, // На втором сервере localIp первого становится remoteIp type: iface.type, server2Name: server1Name, server2Info: server1 }); }); // Сортируем серверы по имени для консистентности const sortedServers = Object.entries(interfacesByServer).sort(([a], [b]) => a.localeCompare(b)); const blocks = []; sortedServers.forEach(([serverName, serverData]) => { const server = serverData.serverInfo; let code = `# Настройка IP адресов интерфейсов для сервера: ${serverName}\n`; if (server.provider) { code += `# Провайдер: ${server.provider}\n`; } if (server.country) { code += `# Страна: ${server.country}\n`; } if (server.ip && server.ip !== serverName) { code += `# IP: ${server.ip}\n`; } code += '\n'; serverData.interfaces.forEach((iface, ifaceIndex) => { code += `# Интерфейс ${ifaceIndex + 1}: ${iface.interfaceName} (${iface.type})\n`; code += `# Связь с сервером: ${iface.server2Name}\n`; code += `# Local IP: ${iface.localIp}\n`; code += `# Remote IP: ${iface.remoteIp}\n`; code += `/ip address add address=${iface.localIp}/32 interface="${iface.interfaceName}" comment="Interface: ${iface.interfaceName} (${iface.type}) to ${iface.server2Name}"\n`; code += '\n'; }); code += '# Проверка настроенных адресов на этом сервере:\n'; code += '# /ip address print\n'; blocks.push({ type: 'interface-addresses', serverName, server, code }); }); return blocks; }; // === Генерация кода MikroTik для рекурсивных маршрутов === const generateMikrotikRecursiveRoutes = () => { const recursiveGateways = (config.gateways || []).filter(gw => gw.type === 'recursive'); const blocks = []; // Если есть рекурсивные gateway, генерируем блоки для них if (recursiveGateways.length > 0) { // Группируем по серверам для удобства const gatewaysByServer = {}; recursiveGateways.forEach(gw => { const serverId = gw.serverId || '__unassigned__'; if (!gatewaysByServer[serverId]) { gatewaysByServer[serverId] = []; } gatewaysByServer[serverId].push(gw); }); // Генерируем блоки для рекурсивных маршрутов по серверам Object.entries(gatewaysByServer).forEach(([serverId, gateways]) => { const server = getServerInfo(serverId); const serverName = server?.dns || server?.ip || serverId; let code = `# Рекурсивные маршруты для сервера: ${serverName}\n`; if (server?.provider) { code += `# Провайдер: ${server.provider}\n`; } if (server?.country) { code += `# Страна: ${server.country}\n`; } code += '\n'; code += '# Удаление существующих рекурсивных маршрутов (опционально)\n'; code += '/ip route remove [find comment~"Recursive"]\n\n'; let validRoutesCount = 0; gateways.forEach(gw => { const parent = getParentGateway(gw.parentGatewayId); if (!parent) { code += `# Gateway "${gw.ip || 'без IP'}" - родительский gateway/интерфейс не найден, пропущен\n`; return; } if (!gw.ip) { code += `# Gateway без IP адреса - пропущен\n`; return; } // Определяем IP родителя в зависимости от типа const parentIp = parent.parentType === 'interface' ? parent.remoteIp : parent.ip; if (!parentIp) { const parentType = parent.parentType === 'interface' ? 'интерфейс' : 'gateway'; code += `# Gateway "${gw.ip}" - родительский ${parentType} не имеет IP, пропущен\n`; return; } validRoutesCount++; // Создаём маршрут для рекурсивного gateway const parentLabel = parent.parentType === 'interface' ? `интерфейс ${parent.name || parent.interfaceType || 'interface'} (${parent.remoteIp})` : `gateway ${parent.ip}`; code += `# Рекурсивный gateway: ${gw.ip}\n`; code += `# Родительский: ${parentLabel}\n`; // Формируем gateway: для интерфейсов добавляем имя через %, для gateway - только IP const gatewayValue = parent.parentType === 'interface' ? `${parentIp}%${parent.name || parent.interfaceType || 'interface'}` : parentIp; // Создаём маршрут к IP рекурсивного gateway через родительский const comment = gw.description ? `${gw.description} -> ${parentIp}` : `Recursive: ${gw.ip} -> ${parentIp}`; code += `/ip route add dst-address=${gw.ip}/32 gateway=${gatewayValue} comment="${comment}"\n`; // Если у рекурсивного gateway есть описание с указанием на default route if (gw.description && (gw.description.toLowerCase().includes('default') || gw.description.toLowerCase().includes('0.0.0.0'))) { code += `# Дополнительный default route через рекурсивный gateway\n`; const defaultComment = gw.description ? `${gw.description} (default)` : `Recursive default: ${gw.ip}`; code += `/ip route add dst-address=0.0.0.0/0 gateway=${gatewayValue} comment="${defaultComment}"\n`; } code += '\n'; }); code += '# Проверка созданных маршрутов:\n'; code += '# /ip route print where comment~"Recursive"\n'; code += `# Всего создано маршрутов: ${validRoutesCount}\n`; if (validRoutesCount > 0) { blocks.push({ type: 'recursive-routes', serverName, server, code }); } }); } // Генерируем блоки для IP адресов интерфейсов const interfaceBlocks = generateMikrotikInterfaceAddresses(); if (interfaceBlocks && interfaceBlocks.length > 0) { blocks.push(...interfaceBlocks); } return blocks; }; // === Обработчик кнопки "Код для MikroTik" === const handleGenerateMikrotikCode = () => { const code = generateMikrotikRecursiveRoutes(); setGeneratedMikrotikCode(code); setMikrotikCodeModalOpen(true); }; // === Копирование кода MikroTik === const handleCopyMikrotikCode = async (codeToCopy) => { if (!codeToCopy) { notify.error('Нет кода для копирования'); return; } try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(codeToCopy); notify.success('Код скопирован в буфер обмена!'); } else { // Fallback для старых браузеров const textArea = document.createElement('textarea'); textArea.value = codeToCopy; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); notify.success('Код скопирован в буфер обмена!'); } catch (err) { notify.error('Не удалось скопировать код'); } document.body.removeChild(textArea); } } catch (err) { console.error('Error copying to clipboard:', err); notify.error('Не удалось скопировать код'); } }; // === Копирование отдельного блока кода === const handleCopyBlock = async (code) => { await handleCopyMikrotikCode(code); }; // === Копирование всего кода === const handleCopyAllMikrotikCode = async () => { const fullCode = Array.isArray(generatedMikrotikCode) && generatedMikrotikCode.length > 0 ? generatedMikrotikCode.map(block => block.code).join('\n\n') : ''; await handleCopyMikrotikCode(fullCode); }; // === Группировка блоков по серверам === const groupedMikrotikCode = useMemo(() => { if (!Array.isArray(generatedMikrotikCode) || generatedMikrotikCode.length === 0) { return {}; } const grouped = {}; generatedMikrotikCode.forEach(block => { const serverName = block.serverName || '__unassigned__'; if (!grouped[serverName]) { grouped[serverName] = { server: block.server, serverName: serverName, blocks: [] }; } grouped[serverName].blocks.push(block); }); return grouped; }, [generatedMikrotikCode]); // === Получение названия типа блока === const getBlockTypeLabel = (type) => { switch (type) { case 'recursive-routes': return 'Рекурсивные маршруты'; case 'interface-addresses': return 'IP адреса интерфейсов'; default: return 'Конфигурация'; } }; // === Получение цвета провайдера === const getProviderColor = (provider) => { if (!provider) return 'secondary'; // Ищем точное совпадение const exact = GATEWAY_PROVIDERS.find(gp => gp.value === provider || gp.label.toLowerCase() === provider.toLowerCase() ); if (exact) return exact.color; // Проверяем частичное совпадение const partial = GATEWAY_PROVIDERS.find(gp => provider.toLowerCase().includes(gp.value.toLowerCase()) || provider.toLowerCase().includes(gp.label.toLowerCase()) ); return partial?.color || 'secondary'; }; const getInterfaceTypeColor = (type) => { const t = INTERFACE_TYPES.find(it => it.value === type); return t?.color || 'secondary'; }; // === Рендер карточки сервера с его элементами === const renderServerGroup = (serverId, items, type) => { const server = getServerInfo(serverId); const isUnassigned = serverId === '__unassigned__'; return (
{server.dns || server.ip || gateway.serverId}
)}
{gatewayType.label}
{gateway.ip && (
)}
{displayServerName}
)}
{iface.type}
{displayIp && displayIp !== '—' && (
)}
{hasActiveFilters ? 'Попробуйте изменить параметры фильтрации' : 'Добавьте gateway для использования в конфигурациях серверов' }
{!hasActiveFilters && ( )}| IP адрес | Тип | Провайдер | Сервер | Страна | Действия |
|---|---|---|---|---|---|
{gateway.ip || '—'}
{gateway.ip && (
)}
{gateway.description}
)}
|
{gatewayType.label}
{parentGateway && (
→ {parentGateway.parentType === 'interface'
? `Интерфейс ${parentGateway.name || parentGateway.interfaceType || 'interface'} (${parentGateway.remoteIp || '—'})`
: parentGateway.ip
? `Gateway ${parentGateway.ip}`
: 'Gateway (не указан IP)'
}
)}
|
{provider ? ( {provider} ) : '—'} |
{server ? (
|
{displayCountry ? (
{countryToFlag(displayCountry)} {displayCountry}
{gatewayCountry && serverCountry && gatewayCountry !== serverCountry && (
) : '—'}
сервер: {serverCountry}
)}
|
|
{hasActiveFilters ? 'Попробуйте изменить параметры фильтрации' : 'Добавьте туннельные интерфейсы для серверов' }
{!hasActiveFilters && ( )}| Имя | Тип | Local IP | Remote IP | Серверы | Действия |
|---|---|---|---|---|---|
| {iface.name || '—'} | {iface.type} |
{iface.localIp || '—'}
{iface.localIp && (
)}
|
{iface.remoteIp || '—'}
{iface.remoteIp && (
)}
|
{server ? (
|
|
{hasActiveFilters ? 'Попробуйте изменить параметры фильтрации' : 'Добавьте пулы IP-адресов для организации сетей' }
{!hasActiveFilters && ( )}{pool.cidr || '—'}
{pool.cidr && (
)}
Вы уверены, что хотите удалить {itemToDelete.ip || itemToDelete.name || itemToDelete.cidr || 'этот элемент'}?
Это действие нельзя отменить.
> ) : null } confirmLabel="Удалить" variant="danger" icon={IconTrash} /> {/* MikroTik Code Modal */} {mikrotikCodeModalOpen && (
{block.code}
Инструкция по применению:
/ip route print или /ip address print