diff --git a/backend/routes/jsonDataRoutes.js b/backend/routes/jsonDataRoutes.js
index cc5a09f..de61278 100644
--- a/backend/routes/jsonDataRoutes.js
+++ b/backend/routes/jsonDataRoutes.js
@@ -9,8 +9,15 @@ const { streamToString } = require('../services/s3Service');
/**
* Создать GET эндпоинт для JSON данных
+ * @param {string} s3Key - Ключ в S3
+ * @param {Object} options - Опции
+ * @param {boolean} options.singleObject - Если true, возвращает объект вместо массива
+ * @param {any} options.defaultValue - Значение по умолчанию ([] для массива, {} для объекта)
*/
-function createJsonDataGET(s3Key) {
+function createJsonDataGET(s3Key, options = {}) {
+ const { singleObject = false, defaultValue } = options;
+ const fallback = defaultValue !== undefined ? defaultValue : (singleObject ? {} : []);
+
return async (req, res) => {
try {
const head = await s3.send(new HeadObjectCommand({ Bucket: BUCKET_NAME, Key: s3Key })).catch(() => null);
@@ -22,22 +29,26 @@ function createJsonDataGET(s3Key) {
const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: s3Key }));
const fileContent = await streamToString(data.Body);
- let items = [];
+ let items = fallback;
try {
- items = JSON.parse(fileContent);
- if (!Array.isArray(items)) {
- items = [];
+ const parsed = JSON.parse(fileContent);
+ if (singleObject) {
+ // Для одиночного объекта
+ items = (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) ? parsed : fallback;
+ } else {
+ // Для массива
+ items = Array.isArray(parsed) ? parsed : fallback;
}
} catch (parseError) {
console.error(`Error parsing ${s3Key}:`, parseError);
- items = [];
+ items = fallback;
}
res.json(items);
} catch (error) {
if (error?.code === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) {
- res.json([]);
+ res.json(fallback);
} else {
console.error(error);
return sendError(res, 500, 'Error reading from S3', 'E_S3');
@@ -48,23 +59,45 @@ function createJsonDataGET(s3Key) {
/**
* Создать POST эндпоинт для JSON данных
+ * @param {string} s3Key - Ключ в S3
+ * @param {Function} validateItem - Функция валидации (для массива - валидирует каждый элемент, для объекта - весь объект)
+ * @param {Object} options - Опции
+ * @param {boolean} options.singleObject - Если true, ожидает объект вместо массива
*/
-function createJsonDataPOST(s3Key, validateItem = null) {
+function createJsonDataPOST(s3Key, validateItem = null, options = {}) {
+ const { singleObject = false } = options;
+
return async (req, res) => {
const { domains: items } = req.body; // Используем 'domains' для обратной совместимости
- if (!Array.isArray(items)) {
- return sendError(res, 400, 'Data must be an array', 'E_BAD_REQUEST');
- }
-
- // Валидация элементов, если предоставлена
- if (validateItem) {
- for (let i = 0; i < items.length; i++) {
- const error = validateItem(items[i], i);
+ if (singleObject) {
+ // Для одиночного объекта
+ if (typeof items !== 'object' || items === null || Array.isArray(items)) {
+ return sendError(res, 400, 'Data must be an object', 'E_BAD_REQUEST');
+ }
+
+ // Валидация объекта целиком
+ if (validateItem) {
+ const error = validateItem(items);
if (error) {
return sendError(res, 400, error, 'E_SCHEMA');
}
}
+ } else {
+ // Для массива
+ if (!Array.isArray(items)) {
+ return sendError(res, 400, 'Data must be an array', 'E_BAD_REQUEST');
+ }
+
+ // Валидация каждого элемента массива
+ if (validateItem) {
+ for (let i = 0; i < items.length; i++) {
+ const error = validateItem(items[i], i);
+ if (error) {
+ return sendError(res, 400, error, 'E_SCHEMA');
+ }
+ }
+ }
}
try {
@@ -79,11 +112,16 @@ function createJsonDataPOST(s3Key, validateItem = null) {
/**
* Создать роуты для JSON данных (GET + POST)
+ * @param {string} s3Key - Ключ в S3
+ * @param {Function} validateItem - Функция валидации
+ * @param {Object} options - Опции
+ * @param {boolean} options.singleObject - Если true, работает с объектом вместо массива
+ * @param {any} options.defaultValue - Значение по умолчанию
*/
-function createJsonDataRoutes(s3Key, validateItem = null) {
+function createJsonDataRoutes(s3Key, validateItem = null, options = {}) {
return {
- get: createJsonDataGET(s3Key),
- post: createJsonDataPOST(s3Key, validateItem)
+ get: createJsonDataGET(s3Key, options),
+ post: createJsonDataPOST(s3Key, validateItem, options)
};
}
diff --git a/backend/server.js b/backend/server.js
index 8e28d5e..f9dbd3a 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -393,6 +393,30 @@ const simpleFiltersRoutes = createJsonDataRoutes('filter-manager/simple-filters.
app.get('/api/simple-filters', simpleFiltersRoutes.get);
app.post('/api/simple-filters', simpleFiltersRoutes.post);
+// Network Config (справочник IP, интерфейсов и gateway)
+const networkConfigRoutes = createJsonDataRoutes('network-config.json', (config) => {
+ // Валидация структуры конфига
+ if (typeof config !== 'object' || config === null) {
+ return 'Network config must be an object';
+ }
+ // Валидация gateways
+ if (config.gateways && !Array.isArray(config.gateways)) {
+ return 'gateways must be an array';
+ }
+ // Валидация tunnelInterfaces
+ if (config.tunnelInterfaces && !Array.isArray(config.tunnelInterfaces)) {
+ return 'tunnelInterfaces must be an array';
+ }
+ // Валидация ipPools
+ if (config.ipPools && !Array.isArray(config.ipPools)) {
+ return 'ipPools must be an array';
+ }
+ return null;
+}, { singleObject: true });
+
+app.get('/api/network-config', networkConfigRoutes.get);
+app.post('/api/network-config', networkConfigRoutes.post);
+
// === COMMUNITIES ROUTES ===
app.get('/api/communities', communitiesRoutes.getCommunities);
app.post('/api/communities', writeLimiter, communitiesRoutes.postCommunities);
diff --git a/example/s3/network-config.json b/example/s3/network-config.json
new file mode 100644
index 0000000..09b9e4d
--- /dev/null
+++ b/example/s3/network-config.json
@@ -0,0 +1,107 @@
+{
+ "gateways": [
+ {
+ "id": "gw-cloudflare-swe",
+ "name": "SWE-IHOR",
+ "ip": "94.142.140.1",
+ "provider": "cloudflare",
+ "country": "SWE",
+ "description": "Cloudflare Sweden - основной выход"
+ },
+ {
+ "id": "gw-bunny-swe",
+ "name": "SWE-BUNNY",
+ "ip": "94.142.140.2",
+ "provider": "bunny",
+ "country": "SWE",
+ "description": "Bunny CDN Sweden"
+ },
+ {
+ "id": "gw-telegram-1",
+ "name": "TG-PROXY-1",
+ "ip": "149.154.167.50",
+ "provider": "telegram",
+ "country": "NL",
+ "description": "Telegram DC2"
+ },
+ {
+ "id": "gw-hetzner-de",
+ "name": "DE-HETZNER",
+ "ip": "195.100.30.21",
+ "provider": "hetzner",
+ "country": "DE",
+ "description": "Hetzner Frankfurt"
+ },
+ {
+ "id": "gw-fastly-us",
+ "name": "US-FASTLY",
+ "ip": "151.101.1.1",
+ "provider": "fastly",
+ "country": "US",
+ "description": "Fastly US East"
+ }
+ ],
+ "tunnelInterfaces": [
+ {
+ "id": "if-gre-swe-de",
+ "name": "gre-swe-de",
+ "type": "GRE",
+ "localIp": "10.10.0.1",
+ "remoteIp": "10.10.0.2",
+ "port": "",
+ "serverId": "SWE-HIPHOST"
+ },
+ {
+ "id": "if-wg-swe-nl",
+ "name": "wg0",
+ "type": "WireGuard",
+ "localIp": "10.20.0.1",
+ "remoteIp": "10.20.0.2",
+ "port": "51820",
+ "serverId": "SWE-HIPHOST"
+ },
+ {
+ "id": "if-gre-de-us",
+ "name": "gre-de-us",
+ "type": "GRE",
+ "localIp": "10.10.1.1",
+ "remoteIp": "10.10.1.2",
+ "port": "",
+ "serverId": "DE-FRANKFURT"
+ },
+ {
+ "id": "if-ipsec-nl-jp",
+ "name": "ipsec-nl-jp",
+ "type": "IPSec",
+ "localIp": "10.30.0.1",
+ "remoteIp": "10.30.0.2",
+ "port": "",
+ "serverId": "NL-AMSTERDAM"
+ }
+ ],
+ "ipPools": [
+ {
+ "id": "pool-gre",
+ "name": "GRE Tunnels",
+ "cidr": "10.10.0.0/16",
+ "description": "Пул для GRE туннелей между серверами"
+ },
+ {
+ "id": "pool-wg",
+ "name": "WireGuard",
+ "cidr": "10.20.0.0/16",
+ "description": "Пул для WireGuard интерфейсов"
+ },
+ {
+ "id": "pool-ipsec",
+ "name": "IPSec VPN",
+ "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/App.jsx b/frontend/src/App.jsx
index 676847a..f6ec100 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -31,6 +31,7 @@ import ASNsNewManager from './ASNsNewManager';
import AutoUrlManager from './AutoUrlManager';
import BillingManager from './BillingManager';
import CommunitiesManager from './CommunitiesManager';
+import NetworkConfigManager from './NetworkConfigManager';
import Dashboard from './Dashboard';
import './App.css';
import { NotifyProvider } from './components/NotifyProvider.jsx';
@@ -51,14 +52,14 @@ function LanguageProvider({ children }) {
home: 'Главная', data: 'Данные', management: 'Управление', tools: 'Инструменты',
dashboard: 'Панель', domains: 'Домены', ipRanges: 'IP-диапазоны', asns: 'AS',
communities: 'Community', servers: 'Серверы', filters: 'Фильтры', billing: 'Биллинг', autoUrls: 'Авто URL',
- easySwitch: 'Easy Switch',
+ easySwitch: 'Easy Switch', networkConfig: 'Сетевые настройки',
light: 'Светлая', dark: 'Тёмная'
},
en: {
home: 'Home', data: 'Data', management: 'Management', tools: 'Tools',
dashboard: 'Dashboard', domains: 'Domains', ipRanges: 'IP Ranges', asns: 'ASNs',
communities: 'Communities', servers: 'Servers', filters: 'Filters', billing: 'Billing', autoUrls: 'Auto URLs',
- easySwitch: 'Easy Switch',
+ easySwitch: 'Easy Switch', networkConfig: 'Network Config',
light: 'Light', dark: 'Dark'
}
};
@@ -190,6 +191,7 @@ function MainLayout() {
items: [
{ id: 'servers', title: t('servers'), path: '/servers', icon: IconServer },
{ id: 'filters', title: t('filters'), path: '/filters', icon: IconFilter },
+ { id: 'network-config', title: t('networkConfig'), path: '/network-config', icon: IconNetwork },
{ id: 'easy-switch', title: t('easySwitch'), path: '/easy-switch', icon: IconArrowsExchange },
{ id: 'billing', title: t('billing'), path: '/billing', icon: IconCreditCard }
]
@@ -397,6 +399,7 @@ function MainLayout() {
} />
} />
} />
+ } />
} />
} />
diff --git a/frontend/src/NetworkConfigManager.jsx b/frontend/src/NetworkConfigManager.jsx
new file mode 100644
index 0000000..f03df00
--- /dev/null
+++ b/frontend/src/NetworkConfigManager.jsx
@@ -0,0 +1,1029 @@
+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 {
+ IconNetwork,
+ IconPlus,
+ IconEdit,
+ IconTrash,
+ IconDatabase,
+ IconServer,
+ IconWorld,
+ IconRouter,
+ IconCopy,
+ IconSearch,
+ IconRefresh,
+ IconSettings
+} 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' },
+ { value: 'WireGuard', label: 'WireGuard' },
+ { value: 'IPSec', label: 'IPSec' },
+ { value: 'VXLAN', label: 'VXLAN' },
+ { value: 'OpenVPN', label: 'OpenVPN' },
+];
+
+// Пустые объекты
+const getEmptyGateway = () => ({
+ id: `gw-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`,
+ name: '',
+ ip: '',
+ provider: 'custom',
+ country: '',
+ description: '',
+});
+
+const getEmptyInterface = () => ({
+ id: `if-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`,
+ name: '',
+ type: 'GRE',
+ localIp: '',
+ remoteIp: '',
+ port: '',
+ serverId: '',
+});
+
+const getEmptyIpPool = () => ({
+ id: `pool-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`,
+ name: '',
+ cidr: '',
+ description: '',
+});
+
+const getDefaultConfig = () => ({
+ gateways: [],
+ tunnelInterfaces: [],
+ ipPools: [],
+ defaults: {
+ baseUrl: 'https://functions.yandexcloud.net/d4eno3im0qgsr4tj5hdo',
+ type: 'routes',
+ version: 'v4.rsc',
+ },
+});
+
+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'); // 'gateways' | 'interfaces' | 'pools' | 'defaults'
+ const [searchTerm, setSearchTerm] = useState('');
+ const [providerFilter, setProviderFilter] = useState('');
+
+ // === 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('');
+
+ // === Загрузка данных ===
+ 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 : [],
+ 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);
+ }
+ };
+
+ 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 filteredGateways = useMemo(() => {
+ let result = [...(config.gateways || [])];
+
+ if (providerFilter) {
+ result = result.filter(g => g.provider === providerFilter);
+ }
+
+ 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)
+ );
+ }
+
+ return result;
+ }, [config.gateways, providerFilter, searchTerm]);
+
+ const filteredInterfaces = useMemo(() => {
+ let result = [...(config.tunnelInterfaces || [])];
+
+ 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)
+ );
+ }
+
+ return result;
+ }, [config.tunnelInterfaces, searchTerm]);
+
+ 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]);
+
+ // === CRUD для Gateways ===
+ const handleAddGateway = () => {
+ setEditingGateway(getEmptyGateway());
+ setGatewayModalMode('add');
+ setGatewayModalOpen(true);
+ };
+
+ const handleEditGateway = (gateway) => {
+ setEditingGateway({ ...gateway });
+ setGatewayModalMode('edit');
+ setGatewayModalOpen(true);
+ };
+
+ const handleSaveGateway = (gatewayData) => {
+ if (gatewayModalMode === 'add') {
+ setConfig(prev => ({
+ ...prev,
+ gateways: [...prev.gateways, gatewayData],
+ }));
+ notify.success('Gateway добавлен');
+ } else {
+ 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('');
+ };
+
+ // === Обновление defaults ===
+ const handleDefaultsChange = (field, value) => {
+ setConfig(prev => ({
+ ...prev,
+ defaults: {
+ ...prev.defaults,
+ [field]: value,
+ },
+ }));
+ };
+
+ // === Copy to clipboard ===
+ const copyToClipboard = async (text) => {
+ try {
+ await navigator.clipboard.writeText(text);
+ notify.success('Скопировано');
+ } catch {
+ notify.error('Не удалось скопировать');
+ }
+ };
+
+ // === Получение имени сервера по 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';
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+ Сетевые настройки
+
+
+ Справочник IP-адресов, интерфейсов и gateway для серверов
+
+
+
+
+
+
+
+
+
+
+
+ {/* Tabs */}
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+ {/* Search & Filters */}
+ {activeTab !== 'defaults' && (
+
+
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+ {activeTab === 'gateways' && (
+
+
+
+ )}
+
+
+ {activeTab === 'gateways' && (
+
+ )}
+ {activeTab === 'interfaces' && (
+
+ )}
+ {activeTab === 'pools' && (
+
+ )}
+
+
+
+
+ )}
+
+ {/* Content */}
+ {loading ? (
+
+ ) : (
+ <>
+ {/* Gateways Tab */}
+ {activeTab === 'gateways' && (
+
+ {filteredGateways.length === 0 ? (
+
+
+
Gateways не найдены
+
Добавьте gateway для использования в конфигурациях
+
+
+ ) : (
+
+
+
+
+ | Имя |
+ IP адрес |
+ Провайдер |
+ Страна |
+ Описание |
+ Действия |
+
+
+
+ {filteredGateways.map(gateway => (
+
+ |
+ {gateway.name || '—'}
+ |
+
+
+ {gateway.ip || '—'}
+ {gateway.ip && (
+
+ )}
+
+ |
+
+
+ {GATEWAY_PROVIDERS.find(p => p.value === gateway.provider)?.label || gateway.provider}
+
+ |
+ {gateway.country || '—'} |
+ {gateway.description || '—'} |
+
+
+
+
+
+ |
+
+ ))}
+
+
+
+ )}
+
+ )}
+
+ {/* Interfaces Tab */}
+ {activeTab === 'interfaces' && (
+
+ {filteredInterfaces.length === 0 ? (
+
+
+
Интерфейсы не найдены
+
Добавьте туннельные интерфейсы для серверов
+
+
+ ) : (
+
+
+
+
+ | Имя |
+ Тип |
+ Local IP |
+ Remote IP |
+ Порт |
+ Сервер |
+ Действия |
+
+
+
+ {filteredInterfaces.map(iface => (
+
+ | {iface.name || '—'} |
+
+ {iface.type}
+ |
+
+
+ {iface.localIp || '—'}
+ {iface.localIp && (
+
+ )}
+
+ |
+
+ {iface.remoteIp || '—'}
+ |
+ {iface.port || '—'} |
+ {getServerName(iface.serverId)} |
+
+
+
+
+
+ |
+
+ ))}
+
+
+
+ )}
+
+ )}
+
+ {/* IP Pools Tab */}
+ {activeTab === 'pools' && (
+
+ {filteredPools.length === 0 ? (
+
+
+
IP пулы не найдены
+
Добавьте пулы IP-адресов для автоматического назначения
+
+
+ ) : (
+
+
+
+
+ | Название |
+ CIDR |
+ Описание |
+ Действия |
+
+
+
+ {filteredPools.map(pool => (
+
+ | {pool.name || '—'} |
+
+
+ {pool.cidr || '—'}
+ {pool.cidr && (
+
+ )}
+
+ |
+ {pool.description || '—'} |
+
+
+
+
+
+ |
+
+ ))}
+
+
+
+ )}
+
+ )}
+
+ {/* Defaults Tab */}
+ {activeTab === 'defaults' && (
+
+
+
Настройки по умолчанию для генерации URL
+
+
+
+
+
+
handleDefaultsChange('baseUrl', e.target.value)}
+ placeholder="https://functions.yandexcloud.net/..."
+ />
+
Базовый URL для генерации ссылок
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ )}
+
+ {/* Gateway Modal */}
+
{ setGatewayModalOpen(false); setEditingGateway(null); }}
+ onSubmit={() => editingGateway && handleSaveGateway(editingGateway)}
+ title={gatewayModalMode === 'add' ? 'Добавить Gateway' : 'Редактировать Gateway'}
+ submitLabel={gatewayModalMode === 'add' ? 'Добавить' : 'Сохранить'}
+ submitIcon={gatewayModalMode === 'add' ? IconPlus : IconEdit}
+ >
+ {editingGateway && (
+
+
+ setEditingGateway({ ...editingGateway, name: val })}
+ placeholder="SWE-IHOR"
+ required
+ />
+
+
+ setEditingGateway({ ...editingGateway, ip: val })}
+ placeholder="94.142.140.1"
+ />
+
+
+ setEditingGateway({ ...editingGateway, provider: val })}
+ options={GATEWAY_PROVIDERS.map(p => ({ value: p.value, label: p.label }))}
+ />
+
+
+ setEditingGateway({ ...editingGateway, country: val })}
+ placeholder="SWE"
+ />
+
+
+ setEditingGateway({ ...editingGateway, description: val })}
+ placeholder="Основной выход через Cloudflare"
+ />
+
+
+ )}
+
+
+ {/* Interface Modal */}
+
{ setInterfaceModalOpen(false); setEditingInterface(null); }}
+ onSubmit={() => editingInterface && handleSaveInterface(editingInterface)}
+ title={interfaceModalMode === 'add' ? 'Добавить интерфейс' : 'Редактировать интерфейс'}
+ submitLabel={interfaceModalMode === 'add' ? 'Добавить' : 'Сохранить'}
+ submitIcon={interfaceModalMode === 'add' ? IconPlus : IconEdit}
+ >
+ {editingInterface && (
+
+
+ setEditingInterface({ ...editingInterface, name: val })}
+ placeholder="gre-tunnel1"
+ required
+ />
+
+
+ setEditingInterface({ ...editingInterface, type: val })}
+ options={INTERFACE_TYPES}
+ />
+
+
+ setEditingInterface({ ...editingInterface, localIp: val })}
+ placeholder="10.10.0.1"
+ />
+
+
+ setEditingInterface({ ...editingInterface, remoteIp: val })}
+ placeholder="10.10.0.2"
+ />
+
+
+ setEditingInterface({ ...editingInterface, port: val })}
+ placeholder="51820"
+ />
+
+
+ setEditingInterface({ ...editingInterface, serverId: val })}
+ options={[
+ { value: '', label: 'Выберите сервер' },
+ ...servers.map(s => ({ value: s.id || s.ip, label: s.dns || s.ip }))
+ ]}
+ />
+
+
+ )}
+
+
+ {/* Pool Modal */}
+
{ setPoolModalOpen(false); setEditingPool(null); }}
+ onSubmit={() => editingPool && handleSavePool(editingPool)}
+ title={poolModalMode === 'add' ? 'Добавить IP пул' : 'Редактировать IP пул'}
+ submitLabel={poolModalMode === 'add' ? 'Добавить' : 'Сохранить'}
+ submitIcon={poolModalMode === 'add' ? IconPlus : IconEdit}
+ >
+ {editingPool && (
+
+
+ setEditingPool({ ...editingPool, name: val })}
+ placeholder="GRE Tunnels"
+ required
+ />
+
+
+ setEditingPool({ ...editingPool, cidr: val })}
+ placeholder="10.10.0.0/24"
+ required
+ />
+
+
+ setEditingPool({ ...editingPool, description: val })}
+ placeholder="Пул для GRE туннелей между серверами"
+ />
+
+
+ )}
+
+
+ {/* Delete Confirmation Modal */}
+
{ setDeleteModalOpen(false); setItemToDelete(null); }}
+ onConfirm={executeDelete}
+ title="Подтверждение удаления"
+ message={
+ itemToDelete ? (
+ <>
+ Вы уверены, что хотите удалить {itemToDelete.name || itemToDelete.cidr || 'этот элемент'}?
+ Это действие нельзя отменить.
+ >
+ ) : null
+ }
+ confirmLabel="Удалить"
+ variant="danger"
+ icon={IconTrash}
+ />
+
+ );
+}
+
+export default NetworkConfigManager;