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 ( +
+ {gateway.ip || '—'}
+
+ {gateway.ip && (
+
+ )}
+ {iface.localIp || '—'}
+ {iface.localIp && (
+
+ )}
+ {iface.remoteIp || '—'}
+ {iface.remoteIp && (
+
+ )}
+ Добавьте gateway для использования в конфигурациях
- ++ {hasActiveFilters + ? 'Попробуйте изменить параметры фильтрации' + : 'Добавьте gateway для использования в конфигурациях серверов' + } +
+ {!hasActiveFilters && ( + + )} +| Имя | -IP адрес | -Провайдер | -Страна | -Описание | -Действия | -||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| - {gateway.name || '—'} - | -
-
- {gateway.ip || '—'}
- {gateway.ip && (
-
- )}
-
- |
- - - {GATEWAY_PROVIDERS.find(p => p.value === gateway.provider)?.label || gateway.provider} - - | -{gateway.country || '—'} | -{gateway.description || '—'} | -
-
-
-
-
- |
+ // Table view
+
| Имя | +IP адрес | +Провайдер | +Сервер | +Страна | +Действия |
|---|
+ {gateway.ip || '—'}
+ {gateway.ip && (
+
+ )}
+
+ Добавьте туннельные интерфейсы для серверов
- ++ {hasActiveFilters + ? 'Попробуйте изменить параметры фильтрации' + : 'Добавьте туннельные интерфейсы для серверов' + } +
+ {!hasActiveFilters && ( + + )} +| Имя | -Тип | -Local IP | -Remote IP | -Порт | -Сервер | -Действия | -||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| {iface.name || '—'} | -- {iface.type} - | -
-
- {iface.localIp || '—'}
- {iface.localIp && (
-
- )}
-
- |
-
- {iface.remoteIp || '—'}
- |
- {iface.port || '—'} | -{getServerName(iface.serverId)} | -
-
-
-
-
- |
+ // Table view
+
| Имя | +Тип | +Local IP | +Remote IP | +Порт | +Сервер | +Действия |
|---|
+ {iface.localIp || '—'}
+ {iface.localIp && (
+
+ )}
+
+ {iface.remoteIp || '—'}Добавьте пулы IP-адресов для автоматического назначения
- ++ {hasActiveFilters + ? 'Попробуйте изменить параметры фильтрации' + : 'Добавьте пулы IP-адресов для организации сетей' + } +
+ {!hasActiveFilters && ( + + )} +| Название | -CIDR | -Описание | -Действия | -
|---|---|---|---|
| {pool.name || '—'} | -
-
- {pool.cidr || '—'}
- {pool.cidr && (
-
- )}
-
- |
- {pool.description || '—'} | -
-
- |
-
{pool.cidr || '—'}
+ {pool.cidr && (
+