From c04b2d726ba7532ef9c668dd8e71f57ac86533f9 Mon Sep 17 00:00:00 2001 From: shats Date: Thu, 22 Jan 2026 10:24:19 +0700 Subject: [PATCH] feat(NetworkConfigManager): enhance server management with serverId integration, improved filtering options, and UI updates for gateways and interfaces --- example/s3/network-config.json | 24 +- frontend/src/NetworkConfigManager.jsx | 1150 ++++++++++++++++--------- 2 files changed, 760 insertions(+), 414 deletions(-) diff --git a/example/s3/network-config.json b/example/s3/network-config.json index 09b9e4d..4d84ad6 100644 --- a/example/s3/network-config.json +++ b/example/s3/network-config.json @@ -6,7 +6,8 @@ "ip": "94.142.140.1", "provider": "cloudflare", "country": "SWE", - "description": "Cloudflare Sweden - основной выход" + "description": "Cloudflare Sweden - основной выход", + "serverId": "SWE-HIPHOST" }, { "id": "gw-bunny-swe", @@ -14,15 +15,17 @@ "ip": "94.142.140.2", "provider": "bunny", "country": "SWE", - "description": "Bunny CDN Sweden" + "description": "Bunny CDN Sweden", + "serverId": "SWE-HIPHOST" }, { - "id": "gw-telegram-1", + "id": "gw-telegram-nl", "name": "TG-PROXY-1", "ip": "149.154.167.50", "provider": "telegram", "country": "NL", - "description": "Telegram DC2" + "description": "Telegram DC2", + "serverId": "NL-AMSTERDAM" }, { "id": "gw-hetzner-de", @@ -30,7 +33,8 @@ "ip": "195.100.30.21", "provider": "hetzner", "country": "DE", - "description": "Hetzner Frankfurt" + "description": "Hetzner Frankfurt", + "serverId": "DE-FRANKFURT" }, { "id": "gw-fastly-us", @@ -38,7 +42,8 @@ "ip": "151.101.1.1", "provider": "fastly", "country": "US", - "description": "Fastly US East" + "description": "Fastly US East", + "serverId": "US-NEWYORK" } ], "tunnelInterfaces": [ @@ -98,10 +103,5 @@ "cidr": "10.30.0.0/16", "description": "Пул для IPSec туннелей" } - ], - "defaults": { - "baseUrl": "https://functions.yandexcloud.net/d4eno3im0qgsr4tj5hdo", - "type": "routes", - "version": "v4.rsc" - } + ] } diff --git a/frontend/src/NetworkConfigManager.jsx b/frontend/src/NetworkConfigManager.jsx index f03df00..aacdcc7 100644 --- a/frontend/src/NetworkConfigManager.jsx +++ b/frontend/src/NetworkConfigManager.jsx @@ -4,6 +4,8 @@ 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 { countryToFlag } from './utils/serverUtils.js'; import { IconNetwork, IconPlus, @@ -16,7 +18,11 @@ import { IconCopy, IconSearch, IconRefresh, - IconSettings + IconFilter, + IconX, + IconCircleFilled, + IconLayoutGrid, + IconList } from '@tabler/icons-react'; /** @@ -37,11 +43,11 @@ const GATEWAY_PROVIDERS = [ // Типы туннельных интерфейсов const INTERFACE_TYPES = [ - { value: 'GRE', label: 'GRE' }, - { value: 'WireGuard', label: 'WireGuard' }, - { value: 'IPSec', label: 'IPSec' }, - { value: 'VXLAN', label: 'VXLAN' }, - { value: 'OpenVPN', label: 'OpenVPN' }, + { 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' }, ]; // Пустые объекты @@ -52,6 +58,7 @@ const getEmptyGateway = () => ({ provider: 'custom', country: '', description: '', + serverId: '', }); const getEmptyInterface = () => ({ @@ -75,11 +82,6 @@ const getDefaultConfig = () => ({ gateways: [], tunnelInterfaces: [], ipPools: [], - defaults: { - baseUrl: 'https://functions.yandexcloud.net/d4eno3im0qgsr4tj5hdo', - type: 'routes', - version: 'v4.rsc', - }, }); function NetworkConfigManager() { @@ -92,9 +94,12 @@ function NetworkConfigManager() { const [saving, setSaving] = useState(false); // === UI State === - const [activeTab, setActiveTab] = useState('gateways'); // 'gateways' | 'interfaces' | 'pools' | 'defaults' + const [activeTab, setActiveTab] = useState('gateways'); const [searchTerm, setSearchTerm] = useState(''); const [providerFilter, setProviderFilter] = useState(''); + const [serverFilter, setServerFilter] = useState(''); + const [typeFilter, setTypeFilter] = useState(''); + const [viewMode, setViewMode] = useState('cards'); // 'cards' | 'table' // === Modals === const [gatewayModalOpen, setGatewayModalOpen] = useState(false); @@ -128,14 +133,12 @@ function NetworkConfigManager() { gateways: Array.isArray(data.gateways) ? data.gateways : [], tunnelInterfaces: Array.isArray(data.tunnelInterfaces) ? data.tunnelInterfaces : [], ipPools: Array.isArray(data.ipPools) ? data.ipPools : [], - defaults: data.defaults || getDefaultConfig().defaults, }); } catch (error) { if (error.response?.status !== 404) { console.error('Error fetching network config:', error); notify.error('Не удалось загрузить сетевые настройки'); } - // Если файла нет - используем дефолтные значения setConfig(getDefaultConfig()); } finally { setLoading(false); @@ -165,6 +168,31 @@ function NetworkConfigManager() { } }; + // === Получение информации о сервере === + 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 || [])]; @@ -173,34 +201,48 @@ function NetworkConfigManager() { result = result.filter(g => g.provider === providerFilter); } + if (serverFilter) { + result = result.filter(g => g.serverId === serverFilter); + } + if (searchTerm) { const term = searchTerm.toLowerCase(); result = result.filter(g => g.name?.toLowerCase().includes(term) || g.ip?.toLowerCase().includes(term) || g.country?.toLowerCase().includes(term) || - g.description?.toLowerCase().includes(term) + g.description?.toLowerCase().includes(term) || + getServerLabel(g.serverId).toLowerCase().includes(term) ); } return result; - }, [config.gateways, providerFilter, searchTerm]); + }, [config.gateways, providerFilter, serverFilter, 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) + i.type?.toLowerCase().includes(term) || + getServerLabel(i.serverId).toLowerCase().includes(term) ); } return result; - }, [config.tunnelInterfaces, searchTerm]); + }, [config.tunnelInterfaces, typeFilter, serverFilter, searchTerm, servers]); const filteredPools = useMemo(() => { let result = [...(config.ipPools || [])]; @@ -217,6 +259,37 @@ function NetworkConfigManager() { 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 key = iface.serverId || '__unassigned__'; + if (!grouped[key]) grouped[key] = []; + grouped[key].push(iface); + }); + return grouped; + }, [filteredInterfaces]); + + // === Сброс фильтров === + const resetFilters = () => { + setSearchTerm(''); + setProviderFilter(''); + setServerFilter(''); + setTypeFilter(''); + }; + + const hasActiveFilters = searchTerm || providerFilter || serverFilter || typeFilter; + // === CRUD для Gateways === const handleAddGateway = () => { setEditingGateway(getEmptyGateway()); @@ -357,17 +430,6 @@ function NetworkConfigManager() { setDeleteType(''); }; - // === Обновление defaults === - const handleDefaultsChange = (field, value) => { - setConfig(prev => ({ - ...prev, - defaults: { - ...prev.defaults, - [field]: value, - }, - })); - }; - // === Copy to clipboard === const copyToClipboard = async (text) => { try { @@ -378,42 +440,247 @@ function NetworkConfigManager() { } }; - // === Получение имени сервера по ID === - const getServerName = (serverId) => { - const server = servers.find(s => s.id === serverId || s.ip === serverId); - return server?.dns || server?.ip || serverId || '—'; - }; - // === Получение цвета провайдера === const getProviderColor = (provider) => { const p = GATEWAY_PROVIDERS.find(gp => gp.value === provider); return p?.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 header */} +
+
+ {isUnassigned ? ( + <> + + + + Без привязки к серверу + + ) : ( + <> + + + +
+ {server?.dns || serverId} + {server && ( + + {server.ip} + {server.country && ( + {countryToFlag(server.country)} {server.country} + )} + + )} +
+ + )} +
+ {items.length} +
+ + {/* Items */} +
+ {items.map(item => ( + type === 'gateway' ? renderGatewayCard(item) : renderInterfaceCard(item) + ))} +
+
+ ); + }; + + // === Рендер карточки Gateway === + const renderGatewayCard = (gateway) => ( +
+
+
+
+ + + +
+
{gateway.name || '—'}
+
+ {GATEWAY_PROVIDERS.find(p => p.value === gateway.provider)?.label || gateway.provider} +
+
+
+ +
+ + +
+
+
+ +
+ + {gateway.ip || '—'} + + {gateway.ip && ( + + )} +
+ + {gateway.country && ( +
+ {countryToFlag(gateway.country)} + {gateway.country} +
+ )} + + {gateway.description && ( +
+ {gateway.description} +
+ )} +
+
+
+ ); + + // === Рендер карточки Interface === + const renderInterfaceCard = (iface) => ( +
+
+
+
+ + + +
+
{iface.name || '—'}
+
+ + {iface.type} + + {iface.port && :{iface.port}} +
+
+
+ +
+ + +
+
+
+ +
+
+
Local IP
+
+ {iface.localIp || '—'} + {iface.localIp && ( + + )} +
+
+
+
Remote IP
+
+ {iface.remoteIp || '—'} + {iface.remoteIp && ( + + )} +
+
+
+
+
+
+ ); + return (
- {/* Header */} -
+ {/* Page Header */} +
+
Управление

- + Сетевые настройки

-
- Справочник IP-адресов, интерфейсов и gateway для серверов -
+ {/* Tabs as button group */} +
+ + + +
+
- {/* Tabs */} + {/* Toolbar */}
-
-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-
-
- - {/* Search & Filters */} - {activeTab !== 'defaults' && ( -
-
-
-
-
- - +
+
+ {/* Search */} +
+
+ + + + setSearchTerm(e.target.value)} + /> + {searchTerm && ( + setSearchTerm('')}> + - setSearchTerm(e.target.value)} - /> -
-
- - {activeTab === 'gateways' && ( -
- -
- )} - -
- {activeTab === 'gateways' && ( - - )} - {activeTab === 'interfaces' && ( - - )} - {activeTab === 'pools' && ( - )}
+ + {/* Server filter */} + {(activeTab === 'gateways' || activeTab === 'interfaces') && uniqueServersInConfig.length > 0 && ( +
+ +
+ )} + + {/* Provider filter for gateways */} + {activeTab === 'gateways' && ( +
+ +
+ )} + + {/* Type filter for interfaces */} + {activeTab === 'interfaces' && ( +
+ +
+ )} + + {/* Reset filters */} + {hasActiveFilters && ( +
+ +
+ )} + +
+ {/* View mode toggle */} + {(activeTab === 'gateways' || activeTab === 'interfaces') && ( +
+ + +
+ )} + + {/* Add button */} + {activeTab === 'gateways' && ( + + )} + {activeTab === 'interfaces' && ( + + )} + {activeTab === 'pools' && ( + + )} +
- )} +
{/* Content */} {loading ? ( @@ -545,280 +838,324 @@ function NetworkConfigManager() { <> {/* Gateways Tab */} {activeTab === 'gateways' && ( -
+ <> {filteredGateways.length === 0 ? ( -
- -

Gateways не найдены

-

Добавьте gateway для использования в конфигурациях

- +
+
+
+ + + +
+

Gateways не найдены

+

+ {hasActiveFilters + ? 'Попробуйте изменить параметры фильтрации' + : 'Добавьте gateway для использования в конфигурациях серверов' + } +

+ {!hasActiveFilters && ( + + )} +
+
+ ) : viewMode === 'cards' ? ( + // Grouped cards view +
+ {Object.entries(gatewaysByServer).map(([serverId, gateways]) => + renderServerGroup(serverId, gateways, 'gateway') + )}
) : ( -
- - - - - - - - - - - - - {filteredGateways.map(gateway => ( - - - - - - - + // Table view +
+
+
ИмяIP адресПровайдерСтранаОписаниеДействия
- {gateway.name || '—'} - - - {gateway.ip || '—'} - {gateway.ip && ( - - )} - - - - {GATEWAY_PROVIDERS.find(p => p.value === gateway.provider)?.label || gateway.provider} - - {gateway.country || '—'}{gateway.description || '—'} -
- - -
-
+ + + + + + + + - ))} - -
ИмяIP адресПровайдерСерверСтранаДействия
+ + + {filteredGateways.map(gateway => { + const server = getServerInfo(gateway.serverId); + return ( + + +
+ + + + {gateway.name || '—'} +
+ + + + {gateway.ip || '—'} + {gateway.ip && ( + + )} + + + + + {GATEWAY_PROVIDERS.find(p => p.value === gateway.provider)?.label || gateway.provider} + + + + {server ? ( +
+ + {server.dns || server.ip} +
+ ) : gateway.serverId ? ( + {gateway.serverId} + ) : ( + + )} + + + {gateway.country ? ( + {countryToFlag(gateway.country)} {gateway.country} + ) : '—'} + + +
+ + +
+ + + ); + })} + + +
)} -
+ )} {/* Interfaces Tab */} {activeTab === 'interfaces' && ( -
+ <> {filteredInterfaces.length === 0 ? ( -
- -

Интерфейсы не найдены

-

Добавьте туннельные интерфейсы для серверов

- +
+
+
+ + + +
+

Интерфейсы не найдены

+

+ {hasActiveFilters + ? 'Попробуйте изменить параметры фильтрации' + : 'Добавьте туннельные интерфейсы для серверов' + } +

+ {!hasActiveFilters && ( + + )} +
+
+ ) : viewMode === 'cards' ? ( + // Grouped cards view +
+ {Object.entries(interfacesByServer).map(([serverId, interfaces]) => + renderServerGroup(serverId, interfaces, 'interface') + )}
) : ( -
- - - - - - - - - - - - - - {filteredInterfaces.map(iface => ( - - - - - - - - + // Table view +
+
+
ИмяТипLocal IPRemote IPПортСерверДействия
{iface.name || '—'} - {iface.type} - - - {iface.localIp || '—'} - {iface.localIp && ( - - )} - - - {iface.remoteIp || '—'} - {iface.port || '—'}{getServerName(iface.serverId)} -
- - -
-
+ + + + + + + + + - ))} - -
ИмяТипLocal IPRemote IPПортСерверДействия
+ + + {filteredInterfaces.map(iface => { + const server = getServerInfo(iface.serverId); + return ( + + {iface.name || '—'} + + + {iface.type} + + + + + {iface.localIp || '—'} + {iface.localIp && ( + + )} + + + {iface.remoteIp || '—'} + {iface.port || '—'} + + {server ? ( +
+ + {server.dns || server.ip} +
+ ) : iface.serverId ? ( + {iface.serverId} + ) : ( + + )} + + +
+ + +
+ + + ); + })} + + +
)} -
+ )} {/* IP Pools Tab */} {activeTab === 'pools' && ( -
+ <> {filteredPools.length === 0 ? ( -
- -

IP пулы не найдены

-

Добавьте пулы IP-адресов для автоматического назначения

- +
+
+
+ + + +
+

IP пулы не найдены

+

+ {hasActiveFilters + ? 'Попробуйте изменить параметры фильтрации' + : 'Добавьте пулы IP-адресов для организации сетей' + } +

+ {!hasActiveFilters && ( + + )} +
) : ( -
- - - - - - - - - - - {filteredPools.map(pool => ( - - - - - - - ))} - -
НазваниеCIDRОписаниеДействия
{pool.name || '—'} - - {pool.cidr || '—'} - {pool.cidr && ( - - )} - - {pool.description || '—'} -
- - +
+ + +
-
+
+ +
+ {pool.cidr || '—'} + {pool.cidr && ( + + )} +
+ + {pool.description && ( +
{pool.description}
+ )} +
+
+
+ ))}
)} -
- )} - - {/* Defaults Tab */} - {activeTab === 'defaults' && ( -
-
-

Настройки по умолчанию для генерации URL

-
-
-
-
- - handleDefaultsChange('baseUrl', e.target.value)} - placeholder="https://functions.yandexcloud.net/..." - /> -
Базовый URL для генерации ссылок
-
-
- - -
-
- - -
-
-
-
+ )} )} @@ -831,9 +1168,20 @@ function NetworkConfigManager() { title={gatewayModalMode === 'add' ? 'Добавить Gateway' : 'Редактировать Gateway'} submitLabel={gatewayModalMode === 'add' ? 'Добавить' : 'Сохранить'} submitIcon={gatewayModalMode === 'add' ? IconPlus : IconEdit} + size="lg" > {editingGateway && (
+
+ + setEditingGateway({ ...editingGateway, serverId: val })} + servers={servers} + placeholder="Выберите сервер..." + /> +
Gateway будет привязан к выбранному серверу
+
{editingInterface && (
+
+ + setEditingInterface({ ...editingInterface, serverId: val })} + servers={servers} + placeholder="Выберите сервер..." + /> +
Интерфейс будет привязан к выбранному серверу
+
-
+
-
+
-
+
-
- setEditingInterface({ ...editingInterface, serverId: val })} - options={[ - { value: '', label: 'Выберите сервер' }, - ...servers.map(s => ({ value: s.id || s.ip, label: s.dns || s.ip })) - ]} - /> -
)}