From 604d45ed920700e3fe80cb2d9db2a6f262b6d3a1 Mon Sep 17 00:00:00 2001 From: shats Date: Sun, 25 Jan 2026 14:20:56 +0700 Subject: [PATCH] feat(NetworkConfigManager): add gateway templates feature for streamlined gateway creation and configuration, enhancing user experience and efficiency --- frontend/src/NetworkConfigManager.jsx | 497 +++++++++++++++++++++++++- 1 file changed, 493 insertions(+), 4 deletions(-) diff --git a/frontend/src/NetworkConfigManager.jsx b/frontend/src/NetworkConfigManager.jsx index 97e56d4..39cb9ad 100644 --- a/frontend/src/NetworkConfigManager.jsx +++ b/frontend/src/NetworkConfigManager.jsx @@ -65,6 +65,64 @@ const GATEWAY_TYPES = [ { value: 'recursive', label: 'Рекурсивный', description: 'Ссылается на другой gateway', color: 'blue' }, ]; +// Шаблоны стандартных наборов gateways +const GATEWAY_TEMPLATES = [ + { + id: 'standard-jumphost', + name: 'Стандартный набор для Jumphost', + description: 'Direct Gateway + Основной + EU-Шлюзы', + gateways: [ + { + type: 'direct', + description: 'Direct Gateway', + ipTemplate: '{baseIp}.1', // Будет заменено на реальный IP + country: '', + }, + { + type: 'recursive', + description: 'Основной', + ipTemplate: '{baseIp}.1', + country: '', + parentGateways: [], // Будет заполнено пользователем + }, + { + type: 'recursive', + description: 'EU-Шлюз (VEESP/HIPHOS)', + ipTemplate: '{baseIp}.2', + country: 'SE', + parentGateways: [], + }, + { + type: 'recursive', + description: 'EU-Шлюз (FIN)', + ipTemplate: '{baseIp}.3', + country: 'FI', + parentGateways: [], + }, + ], + }, + { + id: 'minimal-set', + name: 'Минимальный набор', + description: 'Direct Gateway + Основной', + gateways: [ + { + type: 'direct', + description: 'Direct Gateway', + ipTemplate: '{baseIp}.1', + country: '', + }, + { + type: 'recursive', + description: 'Основной', + ipTemplate: '{baseIp}.1', + country: '', + parentGateways: [], + }, + ], + }, +]; + // Пустые объекты const getEmptyGateway = () => ({ id: `gw-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`, @@ -140,6 +198,13 @@ function NetworkConfigManager() { const [itemToDelete, setItemToDelete] = useState(null); const [deleteType, setDeleteType] = useState(''); + // === Gateway Template Modal === + const [gatewayTemplateModalOpen, setGatewayTemplateModalOpen] = useState(false); + const [selectedTemplate, setSelectedTemplate] = useState(null); + const [templateServerId, setTemplateServerId] = useState(''); + const [templateBaseIp, setTemplateBaseIp] = useState(''); // Базовый IP для генерации (например, 10.9.9) + const [templateGateways, setTemplateGateways] = useState([]); // Массив gateways из шаблона с возможностью редактирования + // === Interface Template Modal === const [interfaceTemplateModalOpen, setInterfaceTemplateModalOpen] = useState(false); const [templateServer1, setTemplateServer1] = useState(''); @@ -469,6 +534,151 @@ function NetworkConfigManager() { const hasActiveFilters = searchTerm || providerFilter || serverFilter || typeFilter || gatewayTypeFilter; + // === Обработка шаблонов gateways === + const handleOpenGatewayTemplate = () => { + setSelectedTemplate(null); + setTemplateServerId(''); + setTemplateBaseIp(''); + setTemplateGateways([]); + setGatewayTemplateModalOpen(true); + }; + + const handleSelectTemplate = (template) => { + setSelectedTemplate(template); + // Инициализируем gateways из шаблона + const initializedGateways = template.gateways.map((gw, index) => { + // Генерируем IP из шаблона, если есть базовый IP + let ip = gw.ipTemplate || ''; + if (templateBaseIp && gw.ipTemplate) { + ip = gw.ipTemplate.replace('{baseIp}', templateBaseIp.trim()); + } + + return { + ...gw, + id: `template-gw-${index}`, + ip: ip, + description: gw.description || '', + country: gw.country || '', + parentGateways: gw.parentGateways ? [...gw.parentGateways] : [], + }; + }); + setTemplateGateways(initializedGateways); + }; + + // Обновление IP адресов при изменении базового IP + useEffect(() => { + if (selectedTemplate && templateBaseIp && templateBaseIp.trim().split('.').length >= 3 && templateGateways.length > 0) { + const updatedGateways = templateGateways.map((gw, index) => { + const originalTemplate = selectedTemplate.gateways[index]; + if (originalTemplate && originalTemplate.ipTemplate) { + const templateIp = originalTemplate.ipTemplate.replace('{baseIp}', templateBaseIp.trim()); + // Обновляем IP только если он соответствует шаблону (последний октет совпадает) или пустой + const currentIpParts = gw.ip ? gw.ip.split('.') : []; + const templateIpParts = templateIp.split('.'); + + // Если IP пустой или последний октет совпадает с шаблоном, обновляем + if (!gw.ip || (currentIpParts.length === 4 && templateIpParts.length === 4 && + currentIpParts[3] === templateIpParts[3])) { + return { ...gw, ip: templateIp }; + } + } + return gw; + }); + // Обновляем только если есть изменения + const hasChanges = updatedGateways.some((gw, i) => gw.ip !== templateGateways[i]?.ip); + if (hasChanges) { + setTemplateGateways(updatedGateways); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [templateBaseIp, selectedTemplate?.id]); + + const handleCreateGatewaysFromTemplate = () => { + if (!selectedTemplate) { + notify.error('Выберите шаблон'); + return; + } + + if (!templateServerId) { + notify.error('Выберите сервер'); + return; + } + + if (!templateBaseIp || !templateBaseIp.trim()) { + notify.error('Укажите базовый IP адрес (например, 10.9.9 или 45.8.248)'); + return; + } + + // Генерируем IP адреса из шаблонов + const baseIpParts = templateBaseIp.trim().split('.'); + if (baseIpParts.length < 3) { + notify.error('Базовый IP должен содержать минимум 3 октета (например, 10.9.9)'); + return; + } + + const newGateways = []; + const usedIps = new Set(config.gateways.map(g => g.ip)); + + templateGateways.forEach((gwTemplate, index) => { + // Используем IP из редактируемого gateway (уже сгенерированный из шаблона) + let ip = gwTemplate.ip || ''; + + // Если IP пустой, пытаемся сгенерировать из шаблона + if (!ip && selectedTemplate.gateways[index]?.ipTemplate) { + ip = selectedTemplate.gateways[index].ipTemplate.replace('{baseIp}', templateBaseIp.trim()); + } + + // Если IP все еще пустой, пропускаем + if (!ip || !ip.trim()) { + notify.warning(`Gateway ${index + 1}: IP адрес не указан, пропущен`); + return; + } + + ip = ip.trim(); + + // Если IP уже используется, пропускаем + if (usedIps.has(ip)) { + notify.warning(`IP ${ip} уже используется, пропущен`); + return; + } + + const newGateway = { + id: `gw-${Date.now()}-${index}-${Math.random().toString(16).slice(2, 6)}`, + ip: ip, + description: gwTemplate.description || '', + serverId: templateServerId, + country: gwTemplate.country || '', + type: gwTemplate.type, + parentGateways: gwTemplate.type === 'recursive' + ? (gwTemplate.parentGateways || []).filter(p => p && p.id) + : [], + }; + + newGateways.push(newGateway); + usedIps.add(ip); + }); + + if (newGateways.length === 0) { + notify.error('Не удалось создать ни одного gateway'); + return; + } + + // Добавляем все gateways + setConfig(prev => ({ + ...prev, + gateways: [...prev.gateways, ...newGateways], + })); + + notify.success(`Создано gateways: ${newGateways.length}`); + + // Закрываем модал и сбрасываем значения + setGatewayTemplateModalOpen(false); + setSelectedTemplate(null); + setTemplateServerId(''); + setTemplateBaseIp(''); + setTemplateGateways([]); + }; + // === CRUD для Gateways === const handleAddGateway = () => { setEditingGateway(getEmptyGateway()); @@ -2964,10 +3174,20 @@ function NetworkConfigManager() { {/* Add button */} {activeTab === 'gateways' && ( - + <> + + + )} {activeTab === 'interfaces' && ( <> @@ -4807,6 +5027,275 @@ function NetworkConfigManager() { )} + + {/* Gateway Template Modal */} + { + setGatewayTemplateModalOpen(false); + setSelectedTemplate(null); + setTemplateServerId(''); + setTemplateBaseIp(''); + setTemplateGateways([]); + }} + onSubmit={handleCreateGatewaysFromTemplate} + title="Создать gateways из шаблона" + submitLabel={selectedTemplate ? `Создать ${templateGateways.length} gateways` : "Выберите шаблон"} + submitIcon={IconSparkles} + size="lg" + submitDisabled={!selectedTemplate || !templateServerId || !templateBaseIp} + > +
+ {/* Выбор шаблона */} +
+ +
+ {GATEWAY_TEMPLATES.map(template => { + // Генерируем предпросмотр IP адресов если есть базовый IP + const previewIps = templateBaseIp && templateBaseIp.trim().split('.').length >= 3 + ? template.gateways.map(gw => { + if (gw.ipTemplate) { + return gw.ipTemplate.replace('{baseIp}', templateBaseIp.trim()); + } + return '—'; + }) + : template.gateways.map(() => '—'); + + return ( +
+
handleSelectTemplate(template)} + > +
+
+
+
{template.name}
+

{template.description}

+
+ + {template.gateways.length} {template.gateways.length === 1 ? 'gateway' : 'gateways'} + + + {template.gateways.filter(g => g.type === 'direct').length} прямых, {template.gateways.filter(g => g.type === 'recursive').length} рекурсивных + +
+ {templateBaseIp && templateBaseIp.trim().split('.').length >= 3 && ( +
+
Предпросмотр IP:
+
+ {previewIps.map((ip, idx) => ( + + {ip} + + ))} +
+
+ )} +
+ {selectedTemplate?.id === template.id && ( + + )} +
+
+
+
+ ); + })} +
+
+ + {/* Общие параметры */} + {selectedTemplate && ( + <> +
+
+
Общие параметры
+
+ +
+ + setTemplateServerId(val)} + servers={servers} + placeholder="Выберите сервер для всех gateways..." + /> +
+ +
+ + setTemplateBaseIp(e.target.value)} + placeholder="10.9.9 или 45.8.248 (минимум 3 октета)" + /> +
+ Базовый IP для генерации адресов gateways. Например, для "10.9.9" будут созданы IP: 10.9.9.1, 10.9.9.2, 10.9.9.3 и т.д. +
+
+ + {/* Редактирование отдельных gateways */} +
+
+
Настройка gateways
+
+ {templateGateways.map((gw, index) => { + const gatewayType = GATEWAY_TYPES.find(t => t.value === gw.type); + return ( +
+
+
+
+ + {gatewayType?.label || gw.type} + + Gateway {index + 1} +
+
+
+
+
+
+ + { + const newGateways = [...templateGateways]; + newGateways[index].description = e.target.value; + setTemplateGateways(newGateways); + }} + placeholder="Название gateway" + /> +
+
+ + { + const newGateways = [...templateGateways]; + newGateways[index].ip = e.target.value; + setTemplateGateways(newGateways); + }} + placeholder={gw.ipTemplate ? gw.ipTemplate.replace('{baseIp}', templateBaseIp || 'X.X.X') : 'IP адрес'} + /> +
+ Шаблон: {gw.ipTemplate || '—'} +
+
+
+ + { + const newGateways = [...templateGateways]; + newGateways[index].country = e.target.value.toUpperCase().slice(0, 2); + setTemplateGateways(newGateways); + }} + placeholder="SE, FI, RU..." + maxLength={2} + /> +
+ {gw.type === 'recursive' && ( +
+ +
+ {(gw.parentGateways || []).map((parent, parentIndex) => { + const parentInfo = getParentGateway(parent.id); + return ( +
+
+ { + const newGateways = [...templateGateways]; + if (!newGateways[index].parentGateways) { + newGateways[index].parentGateways = []; + } + newGateways[index].parentGateways[parentIndex] = { ...newGateways[index].parentGateways[parentIndex], id: val }; + setTemplateGateways(newGateways); + }} + gateways={config.gateways} + interfaces={config.tunnelInterfaces} + placeholder="Выберите родительский gateway или интерфейс..." + /> +
+
+ { + const newGateways = [...templateGateways]; + if (!newGateways[index].parentGateways) { + newGateways[index].parentGateways = []; + } + newGateways[index].parentGateways[parentIndex] = { + ...newGateways[index].parentGateways[parentIndex], + distance: e.target.value ? parseInt(e.target.value) : undefined + }; + setTemplateGateways(newGateways); + }} + placeholder="Distance" + /> +
+ +
+ ); + })} + +
+
+ )} +
+
+
+ ); + })} +
+
+ + )} +
+
); }