import { useState, useEffect, useRef } from 'react'; import { IconPlus, IconEdit, IconTrash, IconServer, IconWorld, IconMapPin, IconRouter, IconPlug } from '@tabler/icons-react'; import api from '../../lib/api.js'; import { makeGateway, normalizeGateways, needsGateways as NEEDS_GATEWAYS, needsTunnel as NEEDS_TUNNEL, SERVER_TYPE_OPTIONS, TUNNEL_TYPES } from '../../utils/serverUtils.js'; /** * Список провайдеров */ const PROVIDER_OPTIONS = [ { value: '', label: 'Выберите провайдера', disabled: true }, { value: 'Yandex', label: 'Yandex Cloud' }, { value: 'Hetzner', label: 'Hetzner' }, { value: 'VPSVILLE', label: 'VPSVILLE' }, { value: 'HOSTVDS', label: 'HOSTVDS' }, { value: 'HIP.hosting', label: 'HIP.hosting' }, { value: 'Ihor-hosting', label: 'Ihor-hosting' }, { value: 'DigitalOcean', label: 'DigitalOcean' }, { value: 'Vultr', label: 'Vultr' }, { value: 'Linode', label: 'Linode' }, { value: 'OVH', label: 'OVH' }, { value: 'AWS', label: 'Amazon Web Services' }, { value: 'Google', label: 'Google Cloud' }, { value: 'Azure', label: 'Microsoft Azure' }, { value: 'Другой', label: 'Другой...' }, ]; /** * Создаёт пустой объект сервера */ const getEmptyServer = () => ({ id: '', ip: '', extIp: '', internalIp: '', dns: '', country: '', provider: '', tunnel: 'GRE', type: 'jumphost', gateway: '', gateways: [makeGateway({ primary: true })], }); /** * Универсальное модальное окно для добавления/редактирования сервера * * @param {Object} props * @param {boolean} props.show - Показывать ли модальное окно * @param {'add'|'edit'} props.mode - Режим работы: 'add' для добавления, 'edit' для редактирования * @param {Object} props.server - Данные сервера (для режима редактирования) * @param {Function} props.onSave - Callback при сохранении (получает объект сервера) * @param {Function} props.onClose - Callback при закрытии * @param {string} props.error - Текст ошибки для отображения */ function ServerModal({ show, mode = 'add', server, onSave, onClose, error: externalError }) { const initializedRef = useRef(false); const [localServer, setLocalServer] = useState(getEmptyServer()); const [customProvider, setCustomProvider] = useState(''); const [internalError, setInternalError] = useState(''); const [mikrotikTesting, setMikrotikTesting] = useState(false); const [mikrotikTestResult, setMikrotikTestResult] = useState(null); const isEditMode = mode === 'edit'; const error = externalError || internalError; /** * Подготовка сервера с нормализованными gateways */ const ensureGateways = (srv) => { if (!srv) return getEmptyServer(); const needs = NEEDS_GATEWAYS(srv.type); let gateways = []; if (needs) { gateways = normalizeGateways(srv.gateways, srv.gateway || srv.dns || srv.ip); if (gateways.length === 0) { gateways = [makeGateway({ primary: true })]; } } return { ...srv, gateways }; }; /** * Инициализация данных при открытии */ useEffect(() => { if (show && !initializedRef.current) { if (isEditMode && server) { const prepared = ensureGateways(server); setLocalServer(prepared); // Проверяем, нужен ли кастомный провайдер const providerExists = PROVIDER_OPTIONS.some(p => p.value === server.provider); if (!providerExists && server.provider) { setCustomProvider(server.provider); setLocalServer(prev => ({ ...prev, provider: 'Другой' })); } } else { setLocalServer(getEmptyServer()); setCustomProvider(''); } setInternalError(''); initializedRef.current = true; } if (!show) { initializedRef.current = false; setMikrotikTestResult(null); } }, [show, server, isEditMode]); if (!show) return null; /** * Обработчик изменения поля */ const handleFieldChange = (field, value) => { setLocalServer(prev => ({ ...prev, [field]: value })); }; /** * Валидация и сохранение */ const handleSubmit = (e) => { e.preventDefault(); setInternalError(''); // Валидация обязательных полей if (!localServer.ip?.trim()) { setInternalError('IP адрес обязателен'); return; } if (!localServer.dns?.trim()) { setInternalError('DNS имя обязательно'); return; } if (!localServer.country?.trim()) { setInternalError('Страна обязательна'); return; } // Провайдер let finalProvider = localServer.provider; if (localServer.provider === 'Другой') { if (!customProvider.trim()) { setInternalError('Введите название провайдера'); return; } finalProvider = customProvider.trim(); } if (!finalProvider) { setInternalError('Провайдер обязателен'); return; } // Шлюзы для jumphost/exit задаются в другом месте (network-config); при сохранении передаём существующие или один по умолчанию const needsGws = NEEDS_GATEWAYS(localServer.type); let finalGateways = []; if (needsGws) { finalGateways = normalizeGateways(localServer.gateways, localServer.gateway || localServer.dns || localServer.ip); if (finalGateways.length === 0) { finalGateways = [makeGateway({ primary: true, name: localServer.dns?.split('.')[0] || 'main' })]; } } const serverToSave = { ...localServer, provider: finalProvider, gateways: finalGateways, }; onSave && onSave(serverToSave); }; return ( <>